Neon

databasePostgreSQLserverlessbranchingscale-to-zerotime-travelcloud-nativeinstant-provisioning

Database Platform

Neon

Overview

Neon is a serverless PostgreSQL platform that separates storage and compute to provide innovative features like Git-style database branching, automatic scale-to-zero, time travel queries, and instant provisioning. It eliminates the complexity of traditional database management, allowing developers to focus on building applications.

Details

Storage and Compute Separation

Neon completely separates PostgreSQL's storage and compute layers, enabling independent scaling and flexible resource management. This innovative architecture ensures compute resources are used only when needed, while storage is managed independently.

Database Branching

One of Neon's most distinctive features is database branching. Using a Git-like copy-on-write mechanism, you can instantly create complete copies of your production database. Each branch includes both schema and data, allowing you to create development, testing, and staging environments with minimal overhead.

Scale-to-Zero

Neon automatically scales compute resources down to zero during application idle time. This feature dramatically reduces costs when not in use, achieving up to 10x cost reduction compared to competitors.

Time Travel and Point-in-Time Recovery

Neon provides time travel functionality that allows access to any historical state of your database. This enables you to examine data history or restore specific point-in-time states.

Pros and Cons

Pros

  • True Serverless: Pay only for what you use, zero cost when idle
  • Instant Branching: Create complete database copies in seconds
  • Auto-scaling: Automatically adjusts resources based on load
  • Full PostgreSQL Compatibility: Complete compatibility with existing PostgreSQL applications
  • Developer-Friendly: Simple API and rich SDK support
  • Branch Reset Feature: Easily sync development branches with latest production state
  • Global Deployment: Low-latency access at edge locations

Cons

  • Relatively New Service: General availability started in April 2024
  • Cold Start: Slight delay when resuming from scale-to-zero
  • Storage Limitations: Free tier has storage and compute restrictions
  • Single Writer Node: Writes scale vertically only
  • Enterprise Features: Some advanced features available only in paid plans

References

Implementation Examples

Setup

# Install Neon CLI
npm install -g neonctl

# Login to your account
neonctl auth

# Create a project
neonctl projects create --name my-project

# Get connection string
neonctl connection-string my-project

Database Branching

# Create a new branch from main
neonctl branches create --project-id <project-id> --name development

# Create branch from specific point in time
neonctl branches create \
  --project-id <project-id> \
  --name feature-branch \
  --parent main \
  --timestamp "2024-12-01T10:00:00Z"

# List branches
neonctl branches list --project-id <project-id>

# Reset branch (sync with parent's latest state)
neonctl branches reset --project-id <project-id> --branch development

Connection and Query Execution

import { Pool } from '@neondatabase/serverless';

// Create connection pool
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// Basic query execution
async function getUsers() {
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users');
    return result.rows;
  } finally {
    client.release();
  }
}

// Transaction handling
async function createUserWithProfile(email, name) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    
    const userResult = await client.query(
      'INSERT INTO users (email) VALUES ($1) RETURNING id',
      [email]
    );
    const userId = userResult.rows[0].id;
    
    await client.query(
      'INSERT INTO profiles (user_id, name) VALUES ($1, $2)',
      [userId, name]
    );
    
    await client.query('COMMIT');
    return userId;
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

// Vercel Edge Function example
export default async function handler(req) {
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
  });

  try {
    const { rows } = await pool.query('SELECT NOW()');
    return new Response(JSON.stringify({ time: rows[0].now }), {
      headers: { 'Content-Type': 'application/json' },
    });
  } finally {
    await pool.end();
  }
}

Scaling and Performance

// Load balancing with read replicas
import { neon } from '@neondatabase/serverless';

// Primary connection (for writes)
const sqlWrite = neon(process.env.DATABASE_URL);

// Read replica connection (read-only)
const sqlRead = neon(process.env.DATABASE_URL_REPLICA);

// Write operation
async function createPost(title, content) {
  const [post] = await sqlWrite`
    INSERT INTO posts (title, content, created_at)
    VALUES (${title}, ${content}, NOW())
    RETURNING *
  `;
  return post;
}

// Read operation (using replica)
async function getPosts(limit = 10) {
  const posts = await sqlRead`
    SELECT * FROM posts
    ORDER BY created_at DESC
    LIMIT ${limit}
  `;
  return posts;
}

// Connection pooling optimization
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximum connections
  idleTimeoutMillis: 30000, // Idle timeout
  connectionTimeoutMillis: 2000, // Connection timeout
});

Time Travel Queries

-- View data at specific point in time
SELECT * FROM orders AS OF SYSTEM TIME '2024-12-01 10:00:00';

-- Track changes over past 24 hours
SELECT 
  o1.*,
  o2.status as previous_status
FROM 
  orders o1
  LEFT JOIN orders AS OF SYSTEM TIME (NOW() - INTERVAL '24 hours') o2
  ON o1.id = o2.id
WHERE 
  o1.updated_at > NOW() - INTERVAL '24 hours'
  AND o1.status != COALESCE(o2.status, '');

-- Restore deleted records
INSERT INTO orders 
SELECT * FROM orders AS OF SYSTEM TIME '2024-12-01 09:00:00'
WHERE id = 12345;

Monitoring and Debugging

// Query performance monitoring
const { Pool } = require('@neondatabase/serverless');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// Measure query execution time
async function executeWithTiming(query, params = []) {
  const start = Date.now();
  const client = await pool.connect();
  
  try {
    const result = await client.query(query, params);
    const duration = Date.now() - start;
    
    console.log({
      query: query.substring(0, 50) + '...',
      duration: `${duration}ms`,
      rowCount: result.rowCount,
    });
    
    return result;
  } finally {
    client.release();
  }
}

// Monitor connection state
pool.on('error', (err, client) => {
  console.error('Unexpected error on idle client', err);
});

pool.on('connect', (client) => {
  console.log('New client connected');
});

pool.on('acquire', (client) => {
  console.log('Client acquired from pool');
});

CI/CD Pipeline Integration

# GitHub Actions example
name: Database Tests

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Create test branch
        run: |
          npx neonctl branches create \
            --project-id ${{ secrets.NEON_PROJECT_ID }} \
            --name pr-${{ github.event.pull_request.number }} \
            --compute \
            --type read_write
      
      - name: Get connection string
        id: connection
        run: |
          CONNECTION_STRING=$(npx neonctl connection-string \
            ${{ secrets.NEON_PROJECT_ID }} \
            --branch pr-${{ github.event.pull_request.number }})
          echo "DATABASE_URL=$CONNECTION_STRING" >> $GITHUB_OUTPUT
      
      - name: Run migrations
        env:
          DATABASE_URL: ${{ steps.connection.outputs.DATABASE_URL }}
        run: npm run db:migrate
      
      - name: Run tests
        env:
          DATABASE_URL: ${{ steps.connection.outputs.DATABASE_URL }}
        run: npm test
      
      - name: Cleanup branch
        if: always()
        run: |
          npx neonctl branches delete \
            pr-${{ github.event.pull_request.number }} \
            --project-id ${{ secrets.NEON_PROJECT_ID }}