home / skills / bobmatnyc / claude-mpm-skills / neon

This skill helps you provision and manage Neon serverless Postgres with autoscaling, branching, and fast deployments for serverless apps.

npx playbooks add skill bobmatnyc/claude-mpm-skills --skill neon

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
19.6 KB
---
name: neon-serverless-postgres
description: Neon serverless Postgres with autoscaling, instant database branching, and zero-downtime deployments. Use when building serverless applications, implementing database branching for dev/staging, or deploying with Vercel/Netlify.
progressive_disclosure:
  entry_point:
    summary: "Neon serverless Postgres with autoscaling, instant database branching, and zero-downtime deployments. Use when building serverless applications, implementing database branching for dev/staging, or ..."
    when_to_use: "When working with neon-serverless-postgres or related functionality."
    quick_start: "1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."
---
# Neon Serverless Postgres Skill

---
progressive_disclosure:
  entry_point:
    summary: "Serverless Postgres with autoscaling, branching, and instant database provisioning"
    when_to_use:
      - "When needing serverless Postgres"
      - "When building edge and serverless apps"
      - "When implementing database branching for dev/staging"
      - "When using Drizzle, Prisma, or raw SQL"
    quick_start:
      - "Create project on Neon console"
      - "Get connection string"
      - "Connect with Drizzle/Prisma/pg"
      - "Deploy with Vercel/Netlify"
  token_estimate:
    entry: 75-90
    full: 3800-4800
---

## Core Concepts

### Neon Architecture
- **Projects**: Top-level container for databases and branches
- **Databases**: Postgres databases within a project
- **Branches**: Git-like database copies for development
- **Compute**: Autoscaling Postgres instances
- **Storage**: Separated from compute for instant branching

### Key Features
- **Serverless**: Pay-per-use, scales to zero
- **Branching**: Instant database copies from any point in time
- **Autoscaling**: Compute scales based on load
- **Instant Provisioning**: Databases ready in seconds
- **Connection Pooling**: Built-in PgBouncer support

## Connection Strings

### Standard Connection
```bash
# Direct connection (for migrations, admin tasks)
DATABASE_URL="postgresql://user:[email protected]/dbname"

# Pooled connection (for application queries)
DATABASE_URL="postgresql://user:[email protected]/dbname?sslmode=require"
```

### Connection Pooling
```bash
# PgBouncer pooled connection (recommended for serverless)
DATABASE_URL="postgresql://user:[email protected]/dbname?sslmode=require"

# Direct connection for migrations
DIRECT_URL="postgresql://user:[email protected]/dbname"
```

## Drizzle ORM Integration

### Setup
```typescript
// drizzle.config.ts
import type { Config } from "drizzle-kit";

export default {
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
} satisfies Config;

// src/db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
```

### Schema Definition
```typescript
// src/db/schema.ts
import { pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  content: text("content"),
  userId: serial("user_id").references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});
```

### Queries
```typescript
import { db } from "./db";
import { users, posts } from "./db/schema";
import { eq } from "drizzle-orm";

// Insert
const newUser = await db.insert(users).values({
  name: "John Doe",
  email: "[email protected]",
}).returning();

// Query
const allUsers = await db.select().from(users);

// Join
const userPosts = await db
  .select()
  .from(posts)
  .leftJoin(users, eq(posts.userId, users.id));

// Update
await db.update(users)
  .set({ name: "Jane Doe" })
  .where(eq(users.id, 1));
```

### Migrations
```bash
# Generate migration
npx drizzle-kit generate:pg

# Run migration (use direct connection)
npx drizzle-kit push:pg

# Or use custom script
# src/db/migrate.ts
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";

const sql = postgres(process.env.DIRECT_URL!, { max: 1 });
const db = drizzle(sql);

await migrate(db, { migrationsFolder: "./drizzle" });
await sql.end();
```

## Prisma Integration

### Setup
```prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  directUrl = env("DIRECT_URL") // For migrations
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  userId    Int
  user      User     @relation(fields: [userId], references: [id])
  createdAt DateTime @default(now())
}
```

### Client Usage
```typescript
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

// Create
const user = await prisma.user.create({
  data: {
    name: "John Doe",
    email: "[email protected]",
  },
});

// Query with relations
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true },
});

// Transaction
await prisma.$transaction([
  prisma.user.create({ data: { name: "User 1", email: "[email protected]" } }),
  prisma.user.create({ data: { name: "User 2", email: "[email protected]" } }),
]);
```

### Migrations
```bash
# Create migration
npx prisma migrate dev --name init

# Deploy to production (uses DIRECT_URL)
npx prisma migrate deploy

# Generate client
npx prisma generate
```

## Node-Postgres (pg) Integration

### Direct Connection
```typescript
import { Pool } from "pg";

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

// Query
const result = await pool.query("SELECT * FROM users WHERE email = $1", [
  "[email protected]",
]);

// Transaction
const client = await pool.connect();
try {
  await client.query("BEGIN");
  await client.query("INSERT INTO users (name, email) VALUES ($1, $2)", [
    "John",
    "[email protected]",
  ]);
  await client.query("COMMIT");
} catch (e) {
  await client.query("ROLLBACK");
  throw e;
} finally {
  client.release();
}
```

### Serverless Driver
```typescript
import { neon, neonConfig } from "@neondatabase/serverless";

// Configure for edge runtime
neonConfig.fetchConnectionCache = true;

const sql = neon(process.env.DATABASE_URL!);

// Execute query
const result = await sql`SELECT * FROM users WHERE email = ${email}`;

// Transactions
const [user] = await sql.transaction([
  sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`,
  sql`INSERT INTO audit_log (action) VALUES ('user_created')`,
]);
```

## Database Branching

### Branch Types
- **Main**: Production branch
- **Development**: Feature development
- **Preview**: PR/deployment previews
- **Testing**: QA and testing environments

### Creating Branches
```bash
# Via CLI
neonctl branches create --name dev --parent main

# Via API
curl -X POST https://console.neon.tech/api/v2/projects/{project_id}/branches \
  -H "Authorization: Bearer $NEON_API_KEY" \
  -d '{"name": "dev", "parent_id": "main"}'

# Via Console
# Navigate to project → Branches → Create branch
```

### Branch Workflows

#### Feature Development
```bash
# 1. Create feature branch
neonctl branches create --name feature/user-auth --parent dev

# 2. Get connection string
neonctl connection-string feature/user-auth

# 3. Update .env.local
DATABASE_URL="postgresql://...feature-user-auth..."

# 4. Run migrations
npm run migrate

# 5. Develop and test

# 6. Merge changes (via schema migration)

# 7. Delete branch
neonctl branches delete feature/user-auth
```

#### Preview Deployments
```typescript
// vercel.json
{
  "env": {
    "DATABASE_URL": "@database-url-main"
  },
  "build": {
    "env": {
      "DATABASE_URL": "@database-url-preview"
    }
  }
}

// Create preview branch on deploy
// .github/workflows/preview.yml
- name: Create Neon Branch
  run: |
    BRANCH_NAME="preview-${{ github.event.number }}"
    neonctl branches create --name $BRANCH_NAME --parent main
    DATABASE_URL=$(neonctl connection-string $BRANCH_NAME)
    echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
```

### Point-in-Time Recovery
```bash
# Create branch from specific timestamp
neonctl branches create --name recovery \
  --parent main \
  --timestamp "2024-01-15T10:30:00Z"

# Restore from branch
neonctl branches reset main --from recovery
```

## Vercel Integration

### Automatic Setup
```bash
# Install Vercel CLI
npm i -g vercel

# Link project
vercel link

# Add Neon integration
vercel integration add neon

# Vercel automatically:
# - Creates main branch connection
# - Creates preview branch per PR
# - Sets DATABASE_URL environment variable
```

### Manual Configuration
```bash
# Add to Vercel project settings
vercel env add DATABASE_URL

# For preview branches
vercel env add DATABASE_URL preview

# For production
vercel env add DATABASE_URL production
```

### Next.js Integration
```typescript
// app/api/users/route.ts
import { neon } from "@neondatabase/serverless";

export const runtime = "edge";

export async function GET() {
  const sql = neon(process.env.DATABASE_URL!);
  const users = await sql`SELECT * FROM users`;

  return Response.json(users);
}

// app/api/users/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const sql = neon(process.env.DATABASE_URL!);
  const [user] = await sql`SELECT * FROM users WHERE id = ${params.id}`;

  if (!user) {
    return new Response("Not found", { status: 404 });
  }

  return Response.json(user);
}
```

## Connection Pooling

### PgBouncer Pooling
```typescript
// Use pooled connection for queries
const pooledDb = drizzle(neon(process.env.DATABASE_URL!));

// Use direct connection for migrations
const directDb = drizzle(neon(process.env.DIRECT_URL!));

// package.json scripts
{
  "scripts": {
    "migrate": "DATABASE_URL=$DIRECT_URL drizzle-kit push:pg",
    "dev": "next dev"
  }
}
```

### Connection Limits
```typescript
// Configure pool size
import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Max connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// For serverless, use Neon's serverless driver
import { neon } from "@neondatabase/serverless";
// Automatically handles connection pooling
```

## Autoscaling and Compute

### Compute Units
```yaml
# Free Tier
- 0.25 Compute Units (CU)
- Scales to zero when idle
- Shared compute

# Pro Tier
- 0.25 - 4 CU autoscaling
- Configurable min/max
- Dedicated compute
```

### Configuration
```bash
# Via CLI
neonctl set-compute --min 0.25 --max 2 --branch main

# Via API
curl -X PATCH https://console.neon.tech/api/v2/projects/{id}/branches/{branch_id} \
  -d '{"compute": {"min_cu": 0.25, "max_cu": 2}}'
```

### Autoscaling Strategy
```typescript
// Development: Scale to zero
// min: 0.25 CU, max: 1 CU

// Staging: Minimal baseline
// min: 0.5 CU, max: 2 CU

// Production: Always-on baseline
// min: 1 CU, max: 4 CU

// Configure per branch
const computeConfig = {
  dev: { min: 0.25, max: 1 },
  staging: { min: 0.5, max: 2 },
  main: { min: 1, max: 4 },
};
```

## Read Replicas

### Setup
```bash
# Create read replica
neonctl read-replica create --branch main --region us-east-1

# Get connection string
neonctl connection-string --replica
```

### Usage Pattern
```typescript
// Write to primary
const writeDb = drizzle(neon(process.env.DATABASE_URL!));

// Read from replica
const readDb = drizzle(neon(process.env.DATABASE_URL_REPLICA!));

// Application logic
async function getUser(id: number) {
  return await readDb.select().from(users).where(eq(users.id, id));
}

async function updateUser(id: number, data: any) {
  return await writeDb.update(users).set(data).where(eq(users.id, id));
}

// Load balancing
const replicas = [
  process.env.DATABASE_URL_REPLICA_1!,
  process.env.DATABASE_URL_REPLICA_2!,
];

function getReadConnection() {
  const url = replicas[Math.floor(Math.random() * replicas.length)];
  return drizzle(neon(url));
}
```

## CLI Usage

### Installation
```bash
npm install -g neonctl

# Or use npx
npx neonctl --help
```

### Common Commands
```bash
# Authentication
neonctl auth

# List projects
neonctl projects list

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

# List branches
neonctl branches list

# Create branch
neonctl branches create --name dev --parent main

# Get connection string
neonctl connection-string main

# Database operations
neonctl databases create --name analytics
neonctl databases list

# Compute settings
neonctl set-compute --min 0.5 --max 2

# Delete branch
neonctl branches delete dev
```

## Migration Strategies

### Drizzle Migrations
```typescript
// drizzle/migrate.ts
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";

const runMigrations = async () => {
  const connection = postgres(process.env.DIRECT_URL!, { max: 1 });
  const db = drizzle(connection);

  console.log("Running migrations...");
  await migrate(db, { migrationsFolder: "./drizzle" });
  console.log("Migrations complete!");

  await connection.end();
};

runMigrations();
```

### Prisma Migrations
```bash
# Development
npx prisma migrate dev --name add_users_table

# Production (uses DIRECT_URL)
npx prisma migrate deploy

# Reset database (dev only)
npx prisma migrate reset
```

### Zero-Downtime Migrations
```sql
-- 1. Add new column (nullable)
ALTER TABLE users ADD COLUMN new_email VARCHAR(255);

-- 2. Backfill data
UPDATE users SET new_email = email;

-- 3. Make non-nullable (after verification)
ALTER TABLE users ALTER COLUMN new_email SET NOT NULL;

-- 4. Drop old column
ALTER TABLE users DROP COLUMN email;

-- 5. Rename column
ALTER TABLE users RENAME COLUMN new_email TO email;
```

### Branch-Based Migrations
```bash
# 1. Create migration branch
neonctl branches create --name migration/add-index --parent main

# 2. Test migration on branch
DATABASE_URL=$(neonctl connection-string migration/add-index) \
  npm run migrate

# 3. Verify on branch
DATABASE_URL=$(neonctl connection-string migration/add-index) \
  npm run test

# 4. Apply to main
npm run migrate:production

# 5. Delete migration branch
neonctl branches delete migration/add-index
```

## Best Practices

### Serverless Optimization
```typescript
// ✅ Use Neon serverless driver for edge
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);

// ✅ Enable connection caching
import { neonConfig } from "@neondatabase/serverless";
neonConfig.fetchConnectionCache = true;

// ✅ Use pooled connections
const pooledUrl = process.env.DATABASE_URL; // -pooler endpoint

// ❌ Don't use standard pg in edge runtime
// import { Pool } from "pg"; // Won't work in edge
```

### Connection Management
```typescript
// ✅ Reuse connections in serverless
let cachedDb: ReturnType<typeof drizzle> | null = null;

function getDb() {
  if (!cachedDb) {
    const sql = neon(process.env.DATABASE_URL!);
    cachedDb = drizzle(sql);
  }
  return cachedDb;
}

// ✅ Use transactions for consistency
await db.transaction(async (tx) => {
  await tx.insert(users).values({ name: "John" });
  await tx.insert(auditLog).values({ action: "user_created" });
});

// ❌ Don't forget to close pools in long-running processes
// await pool.end();
```

### Branch Strategy
```yaml
Environments:
  main: Production data
  staging: Pre-production testing
  dev: Shared development
  feature/*: Individual features
  preview/*: PR previews (auto-created)

Lifecycle:
  - Create from parent on feature start
  - Run migrations independently
  - Test thoroughly
  - Merge schema changes
  - Delete after feature completion
```

### Cost Optimization
```typescript
// Development: Scale to zero
// - min_cu: 0.25
// - Suspend after 5 minutes idle

// Staging: Minimal always-on
// - min_cu: 0.5
// - Reduce during off-hours

// Production: Right-size baseline
// - min_cu: Based on traffic patterns
// - max_cu: Handle peak load

// Branch cleanup
// Delete unused preview branches after PR merge
```

## Environment Variables

### Required Variables
```bash
# Neon connection strings
DATABASE_URL="postgresql://user:[email protected]/db?sslmode=require"
DIRECT_URL="postgresql://user:[email protected]/db?sslmode=require"

# API access (for CLI/automation)
NEON_API_KEY="your_api_key"

# Project configuration
NEON_PROJECT_ID="your_project_id"
```

### Multi-Environment Setup
```bash
# .env.local (development)
DATABASE_URL="postgresql://...dev-branch..."

# .env.staging
DATABASE_URL="postgresql://...staging-branch..."

# .env.production (via Vercel)
DATABASE_URL="postgresql://...main-branch..."
```

## Common Patterns

### API Route with Caching
```typescript
import { neon } from "@neondatabase/serverless";

export const runtime = "edge";

export async function GET() {
  const sql = neon(process.env.DATABASE_URL!);

  const users = await sql`SELECT * FROM users ORDER BY created_at DESC LIMIT 10`;

  return Response.json(users, {
    headers: {
      "Cache-Control": "s-maxage=60, stale-while-revalidate",
    },
  });
}
```

### Server Actions (Next.js)
```typescript
"use server";

import { neon } from "@neondatabase/serverless";
import { revalidatePath } from "next/cache";

export async function createUser(formData: FormData) {
  const sql = neon(process.env.DATABASE_URL!);

  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`;

  revalidatePath("/users");
}
```

### Connection Testing
```typescript
async function testConnection() {
  const sql = neon(process.env.DATABASE_URL!);

  try {
    const result = await sql`SELECT version()`;
    console.log("✅ Connected to Neon:", result[0].version);
    return true;
  } catch (error) {
    console.error("❌ Connection failed:", error);
    return false;
  }
}
```

## Troubleshooting

### Connection Issues
```typescript
// Check SSL requirement
const url = new URL(process.env.DATABASE_URL!);
if (!url.searchParams.has("sslmode")) {
  url.searchParams.set("sslmode", "require");
}

// Verify endpoint type
// -pooler: For application queries
// direct: For migrations and admin tasks

// Test connectivity
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
await sql`SELECT 1`; // Should succeed
```

### Migration Failures
```bash
# Use direct connection for migrations
export DIRECT_URL="postgresql://...direct-endpoint..."
npx prisma migrate deploy

# Check migration status
npx prisma migrate status

# Force reset (dev only)
npx prisma migrate reset
```

### Performance Issues
```typescript
// Enable query logging
import { drizzle } from "drizzle-orm/neon-http";
const db = drizzle(sql, { logger: true });

// Check slow queries in Neon console
// Monitoring → Query Performance

// Add indexes
await sql`CREATE INDEX idx_users_email ON users(email)`;

// Use connection pooling
// Ensure using -pooler endpoint
```

This skill provides comprehensive coverage of Neon serverless Postgres, including database branching, ORM integrations, serverless deployment patterns, and production best practices.

Overview

This skill packages guidance and examples for using Neon serverless Postgres with autoscaling, instant branching, and zero-downtime deployments. It focuses on practical integrations with ORMs (Drizzle, Prisma), raw pg usage, serverless drivers, and deployment platforms like Vercel and Netlify. Use the recipes to provision branches, configure pooling, and run safe migrations for serverless apps.

How this skill works

The skill explains Neon architecture (projects, databases, branches, compute, storage) and shows concrete connection and pooling patterns (pooled DATABASE_URL vs DIRECT_URL for migrations). It provides code snippets for Drizzle, Prisma, node-postgres and Neon's serverless driver, plus CLI/API commands to create branches, set compute limits, and provision read replicas. It also covers branch workflows for feature development, preview deployments, and point-in-time recovery.

When to use it

  • Building edge or serverless applications where connections scale to zero
  • Implementing branch-based development, preview environments, or per-PR databases
  • Running migrations or schema changes safely with a direct connection
  • Deploying on Vercel/Netlify and needing preview databases per deployment
  • Using Drizzle, Prisma, or raw pg in a serverless runtime

Best practices

  • Use pooled connection strings (PgBouncer) for application queries and DIRECT_URL for migrations
  • Create short-lived branches for feature work and preview deployments, then delete when done
  • Configure compute per branch: scale-to-zero for dev, minimal baseline for staging, always-on for production
  • Run zero-downtime migrations: add nullable columns, backfill, then make non-nullable and swap
  • Cache or reuse Neon's serverless driver connections in edge runtimes (fetchConnectionCache=true)

Example use cases

  • Create a preview branch per GitHub PR and set DATABASE_URL in CI to run tests against an isolated database
  • Deploy a Next.js edge API that queries Neon via @neondatabase/serverless with pooled connections
  • Run Prisma/Drizzle migrations using DIRECT_URL while application uses pooled DATABASE_URL
  • Create read replicas for heavy read traffic and route reads to replicas while writes go to primary
  • Restore production state by creating a branch from a timestamp and testing migrations there before resetting main

FAQ

When should I use the pooled URL vs the direct URL?

Use the pooled (PgBouncer) DATABASE_URL for application queries in serverless/edge runtimes. Use DIRECT_URL for migrations and admin tasks that require a direct Postgres connection.

How do I get instant isolated databases for PR previews?

Create a Neon branch from main on deploy (CLI/API/Console). Neon branches are instant because storage is separated from compute; set the branch's DATABASE_URL for the preview environment.