Claude Code for Neon: Serverless PostgreSQL with Branching and Autoscaling — Claude Skills 360 Blog
Blog / Backend / Claude Code for Neon: Serverless PostgreSQL with Branching and Autoscaling
Backend

Claude Code for Neon: Serverless PostgreSQL with Branching and Autoscaling

Published: January 27, 2027
Read time: 8 min read
By: Claude Skills 360

Neon separates PostgreSQL storage from compute — compute scales to zero when idle and warms in under a second. The @neondatabase/serverless driver uses HTTP and WebSockets instead of TCP, enabling PostgreSQL queries from Cloudflare Workers, Vercel Edge Functions, and any environment where Node.js TCP connections aren’t available. Database branching creates instant copy-on-write snapshots — each pull request gets its own branch with production data, automatically deprovisioned after merge. Autoscaling adjusts vCPUs and RAM dynamically. Drizzle ORM works with Neon’s serverless driver identically to standard PostgreSQL. Claude Code generates Neon connection configs, serverless edge queries, branch provisioning scripts, Drizzle schemas, and the autoscaling configurations for production PostgreSQL applications.

CLAUDE.md for Neon Projects

## Neon Stack
- Driver: @neondatabase/serverless >= 0.10 for edge/serverless; pg for long-running Node.js
- ORM: drizzle-orm >= 0.34 with drizzle-orm/neon-http (serverless) or drizzle-orm/pg-core (node)
- Branching: neon branches create/delete via Neon API for CI/CD preview envs
- Pooling: use pooled connection string (port 5432 → 6543 PgBouncer) for serverless
- Autoscale: set min/max compute units in console or API — 0.25 CU min = scale to zero
- Migrations: drizzle-kit push (dev) or drizzle-kit generate + drizzle-kit migrate (prod)

Serverless/Edge Connection

// lib/db.ts — Neon serverless driver setup
import { neon, neonConfig } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-http"
import * as schema from "./schema"

// Enable WebSocket pooling for repeated queries in same invocation
neonConfig.fetchConnectionCache = true

// HTTP-based client — works in Cloudflare Workers, Vercel Edge, etc.
const sql = neon(process.env.DATABASE_URL!)

export const db = drizzle(sql, { schema })

// Direct SQL for complex queries
export { sql }
// lib/db-node.ts — standard driver for Node.js / long-running servers
import { Pool } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-serverless"
import ws from "ws"
import * as schema from "./schema"
import { neonConfig } from "@neondatabase/serverless"

// Required for WebSocket in Node.js environments
neonConfig.webSocketConstructor = ws

// Use pooled connection string for PgBouncer
const pool = new Pool({ connectionString: process.env.DATABASE_URL_POOLED! })

export const db = drizzle(pool, { schema })

Drizzle Schema

// lib/schema.ts — Drizzle PostgreSQL schema for Neon
import {
  pgTable, pgEnum, text, integer, timestamp, boolean,
  index, uniqueIndex, uuid, jsonb
} from "drizzle-orm/pg-core"
import { relations } from "drizzle-orm"

export const orderStatusEnum = pgEnum("order_status", [
  "pending", "processing", "shipped", "delivered", "cancelled"
])

export const customers = pgTable("customers", {
  id: uuid("id").primaryKey().defaultRandom(),
  email: text("email").notNull(),
  name: text("name").notNull(),
  stripeCustomerId: text("stripe_customer_id"),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  emailIdx: uniqueIndex("customers_email_idx").on(table.email),
  stripeIdx: index("customers_stripe_idx").on(table.stripeCustomerId),
}))

export const orders = pgTable("orders", {
  id: uuid("id").primaryKey().defaultRandom(),
  customerId: uuid("customer_id")
    .references(() => customers.id, { onDelete: "restrict" })
    .notNull(),
  status: orderStatusEnum("status").notNull().default("pending"),
  totalCents: integer("total_cents").notNull(),
  items: jsonb("items").$type<OrderItem[]>().notNull(),
  shippingAddress: jsonb("shipping_address").$type<Address>(),
  trackingNumber: text("tracking_number"),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  customerIdx: index("orders_customer_idx").on(table.customerId),
  statusIdx: index("orders_status_idx").on(table.status),
  createdAtIdx: index("orders_created_at_idx").on(table.createdAt),
}))

export const ordersRelations = relations(orders, ({ one }) => ({
  customer: one(customers, {
    fields: [orders.customerId],
    references: [customers.id],
  }),
}))

Queries and Mutations

// lib/queries.ts — type-safe Drizzle queries
import { db, sql } from "./db"
import { orders, customers } from "./schema"
import { eq, desc, and, gte, lte, count, sum, avg } from "drizzle-orm"

export async function getOrderWithCustomer(orderId: string) {
  return await db.query.orders.findFirst({
    where: eq(orders.id, orderId),
    with: { customer: true },
  })
}

export async function listCustomerOrders(
  customerId: string,
  opts: { limit?: number; offset?: number; status?: string } = {}
) {
  const { limit = 20, offset = 0, status } = opts

  return await db
    .select()
    .from(orders)
    .where(
      and(
        eq(orders.customerId, customerId),
        status ? eq(orders.status, status as any) : undefined
      )
    )
    .orderBy(desc(orders.createdAt))
    .limit(limit)
    .offset(offset)
}

export async function getRevenueStats(startDate: Date, endDate: Date) {
  const [stats] = await db
    .select({
      orderCount: count(),
      totalRevenueCents: sum(orders.totalCents),
      avgOrderCents: avg(orders.totalCents),
    })
    .from(orders)
    .where(
      and(
        gte(orders.createdAt, startDate),
        lte(orders.createdAt, endDate),
        eq(orders.status, "delivered")
      )
    )

  return {
    orderCount: Number(stats.orderCount),
    totalRevenueCents: Number(stats.totalRevenueCents ?? 0),
    avgOrderCents: Number(stats.avgOrderCents ?? 0),
  }
}

// Raw SQL for complex analytics
export async function getTopCustomersByRevenue(limit = 10) {
  return await sql`
    SELECT
      c.id,
      c.email,
      c.name,
      COUNT(o.id) as order_count,
      SUM(o.total_cents) as total_cents
    FROM customers c
    JOIN orders o ON c.id = o.customer_id
    WHERE o.status != 'cancelled'
    GROUP BY c.id, c.email, c.name
    ORDER BY total_cents DESC
    LIMIT ${limit}
  `
}

Cloudflare Worker with Neon

// src/worker.ts — Neon in Cloudflare Worker
import { neon } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-http"
import * as schema from "./schema"

interface Env {
  DATABASE_URL: string
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sql = neon(env.DATABASE_URL)
    const db = drizzle(sql, { schema })

    const url = new URL(request.url)

    if (url.pathname.startsWith("/api/orders")) {
      const customerId = url.searchParams.get("customer_id")
      if (!customerId) return Response.json({ error: "Missing customer_id" }, { status: 400 })

      const customerOrders = await db
        .select()
        .from(schema.orders)
        .where(eq(schema.orders.customerId, customerId))
        .orderBy(desc(schema.orders.createdAt))
        .limit(20)

      return Response.json(customerOrders)
    }

    return Response.json({ error: "Not found" }, { status: 404 })
  },
}

Branch Provisioning for Preview Environments

// scripts/create-preview-branch.ts — Neon API branching
const NEON_API = "https://console.neon.tech/api/v2"
const API_KEY = process.env.NEON_API_KEY!
const PROJECT_ID = process.env.NEON_PROJECT_ID!

interface NeonBranch {
  id: string
  name: string
  connection_string: string
}

export async function createPreviewBranch(prNumber: number): Promise<NeonBranch> {
  const branchName = `preview/pr-${prNumber}`

  // Create branch from main
  const createResponse = await fetch(
    `${NEON_API}/projects/${PROJECT_ID}/branches`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        branch: {
          name: branchName,
          parent_id: "main",  // Branch from main
        },
        endpoints: [{
          type: "read_write",
          autoscaling_limit_min_cu: 0.25,
          autoscaling_limit_max_cu: 1,
        }],
      }),
    }
  )

  const { branch, endpoints } = await createResponse.json()

  // Get connection string
  const connResponse = await fetch(
    `${NEON_API}/projects/${PROJECT_ID}/connection_uri?branch_id=${branch.id}&role_name=neondb_owner&database_name=neondb`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  )
  const { uri } = await connResponse.json()

  return { id: branch.id, name: branchName, connection_string: uri }
}

export async function deletePreviewBranch(prNumber: number): Promise<void> {
  // List branches to find by name
  const listResponse = await fetch(
    `${NEON_API}/projects/${PROJECT_ID}/branches`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  )
  const { branches } = await listResponse.json()

  const branch = branches.find((b: any) => b.name === `preview/pr-${prNumber}`)
  if (!branch) return

  await fetch(
    `${NEON_API}/projects/${PROJECT_ID}/branches/${branch.id}`,
    {
      method: "DELETE",
      headers: { Authorization: `Bearer ${API_KEY}` },
    }
  )
}
# .github/workflows/preview.yml — auto-provision and deprovision Neon branches
name: Preview Environment

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Create Neon branch
        if: github.event.action != 'closed'
        id: neon
        run: |
          OUTPUT=$(npx tsx scripts/create-preview-branch.ts ${{ github.event.pull_request.number }})
          echo "connection_string=$(echo $OUTPUT | jq -r .connection_string)" >> $GITHUB_OUTPUT
        env:
          NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
          NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}

      - name: Run migrations on branch
        if: github.event.action != 'closed'
        run: npm run db:migrate
        env:
          DATABASE_URL: ${{ steps.neon.outputs.connection_string }}

      - name: Delete Neon branch
        if: github.event.action == 'closed'
        run: npx tsx scripts/create-preview-branch.ts delete ${{ github.event.pull_request.number }}
        env:
          NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
          NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}

For the PlanetScale MySQL alternative that offers similar branching workflows with a non-blocking schema change workflow instead of PostgreSQL, the database migrations guide covers multi-environment migration patterns. For Turso SQLite-at-the-edge when PostgreSQL semantics aren’t required and sub-millisecond edge reads matter most, see the Turso guide for libSQL and Drizzle integration. The Claude Skills 360 bundle includes Neon skill sets covering serverless connections, branching workflows, and Drizzle integration. Start with the free tier to try Neon PostgreSQL configuration generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free