Claude Skill

api-baas-neon

Serverless PostgreSQL with branching, autoscaling, and edge-compatible driver

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-baas-neon_skills_api-baas-neon-3a51ef5.zip · 16 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-baas-neon/skills/api-baas-neon
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Neon Serverless PostgreSQL Patterns

Quick Guide: Use @neondatabase/serverless for edge/serverless database access. Prefer the neon() HTTP function for single queries (faster, stateless) and Pool/Client for interactive transactions. Use pooled connection strings (-pooler suffix) for serverless workloads, direct connections only for migrations. Branch your database for dev/preview environments using copy-on-write semantics. Always handle cold starts from scale-to-zero (200-500ms wake-up).


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use the neon() HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)

(You MUST close Pool/Client connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)

(You MUST use pooled connection strings (-pooler suffix) for serverless workloads -- direct connections exhaust the limited connection slots)

(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)

(You MUST use sql.unsafe() only for trusted, known-safe strings like table/column names -- never for user input)

</critical_requirements>


Auto-detection: Neon, @neondatabase/serverless, neon(), neonConfig, neon serverless driver, neon database, neon branch, neonctl, neon connection pooling, neon scale-to-zero, neon autoscaling, neon postgres, ep-*-pooler

When to use:

  • Querying Postgres from edge/serverless functions (edge runtimes, serverless platforms)
  • Setting up connection strings (pooled vs direct) for different workloads
  • Creating database branches for dev, preview, or CI environments
  • Managing scale-to-zero behavior and cold start optimization
  • Running transactions in serverless contexts (HTTP batch or WebSocket)
  • Programmatic branch management via Neon API or neonctl CLI

Key patterns covered:

  • neon() HTTP queries with SQL tagged templates and composable fragments
  • Pool/Client WebSocket connections with proper lifecycle management
  • Pooled (-pooler) vs direct connection strings and when to use each
  • Database branching (dev branches, PR preview branches, schema-only branches)
  • Scale-to-zero behavior, cold start mitigation, and autoscaling
  • sql.transaction() for non-interactive HTTP transactions
  • Neon API and neonctl CLI for programmatic branch management

When NOT to use:

  • Traditional long-lived server connections (use standard pg driver with TCP)
  • Complex ORM-specific patterns (use your ORM's own skill)
  • General PostgreSQL query syntax (use a SQL/Postgres skill)

Detailed Resources:

  • For decision frameworks and quick lookup tables, see reference.md

Driver & Queries:

  • examples/core.md -- Driver setup, HTTP queries, WebSocket connections, transactions

Branching & Operations:




<decision_framework>

Decision Framework

HTTP (neon()) vs WebSocket (Pool/Client)

What kind of database operation?
+-- Single query (SELECT, INSERT, UPDATE, DELETE)
|   +-- YES --> Use neon() HTTP function (fastest, ~3 round trips)
+-- Multiple queries that must be atomic?
|   +-- Can all queries be determined upfront (non-interactive)?
|   |   +-- YES --> Use sql.transaction() over HTTP
|   |   +-- NO --> Use Pool/Client over WebSocket
+-- Need node-postgres (pg) API compatibility?
|   +-- YES --> Use Pool/Client over WebSocket
+-- Running in edge runtime (no TCP)?
    +-- YES --> Use @neondatabase/serverless (HTTP or WebSocket)
    +-- NO --> Standard pg driver with TCP may be simpler

Pooled vs Direct Connection

What is the workload?
+-- Serverless function / edge function --> Pooled (-pooler)
+-- Web application (many concurrent requests) --> Pooled (-pooler)
+-- Schema migration --> Direct (needs session state)
+-- pg_dump / pg_restore --> Direct (uses SET statements)
+-- LISTEN / NOTIFY --> Direct (session-level feature)
+-- Long-running analytics query --> Direct (avoid pool contention)
+-- Default / unsure --> Pooled (-pooler)

Branch Strategy

What do you need the branch for?
+-- Developer working on a feature --> Dev branch (long-lived, manually managed)
+-- PR preview environment --> Preview branch (TTL expiration, auto-cleanup on merge)
+-- CI test run --> Ephemeral branch (short TTL, schema-only if data-sensitive)
+-- Database recovery --> Restore from branch history (up to 30 days on Scale plan)
+-- Load testing --> Branch from production (copy-on-write, no storage cost until diverge)

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Global Pool in serverless -- Creating a Pool outside the request handler in edge/serverless functions leaks WebSocket connections. Pool/Client must be created, used, and closed within a single request.
  • Using direct connection string in serverless -- Direct connections bypass PgBouncer and are limited to compute-size max connections (100-4,000). Serverless functions should always use pooled (-pooler) connections.
  • Passing user input to sql.unsafe() -- sql.unsafe() embeds raw SQL without parameterization. It exists only for trusted identifiers (table/column names). User input in sql.unsafe() is a SQL injection vulnerability.

Medium Priority Issues:

  • Double pooling -- Combining Neon's server-side PgBouncer with a client-side connection pool in your driver creates unnecessary overhead. Let Neon handle pooling.
  • Ignoring pool.end() in serverless -- Forgetting to call pool.end() after using WebSocket connections exhausts available connections across invocations.
  • Using SET statements through pooled connections -- PgBouncer transaction mode resets session state after each transaction. Use ALTER ROLE ... SET for role-level defaults or use direct connections.
  • Not handling cold start latency -- First request after idle period adds 200-500ms. Without appropriate timeouts (10+ seconds) and retry logic, applications fail intermittently.

Common Mistakes:

  • Wrong package name -- The package is @neondatabase/serverless, not neon-serverless or pg-neon.
  • Missing ws package on Node.js <= v21 -- Node.js versions before v22 lack built-in WebSocket support. When using Pool/Client, install ws and set neonConfig.webSocketConstructor = ws. Node.js v22+ has native WebSocket and needs no extra setup.
  • Calling neon() result as a function instead of tagged template -- sql("SELECT ...") is a type error since v1.0. Use sql`SELECT ...` (tagged template).
  • Expecting Pool to survive across serverless invocations -- Each cold start creates a new execution context. Do not rely on global state for connection management.
  • 64MB request/response limit -- HTTP mode has a 64MB payload limit. Large result sets or bulk inserts must be chunked.

Gotchas & Edge Cases:

  • Transaction options apply to the transaction, not individual queries -- Setting arrayMode: true on individual queries inside sql.transaction() is ignored. Set it on the transaction itself.
  • PgBouncer's 120-second query wait timeout -- If all pooled connections are busy, new queries queue for up to 120 seconds before timing out.
  • Branch endpoints are different from parent -- Each branch gets a unique endpoint ID. You cannot use the parent's connection string to connect to a child branch.
  • Scale-to-zero only for computes <= 16 CU -- Computes larger than 16 CU remain always-on regardless of configuration.
  • Logical replication prevents suspension -- Active replication subscribers keep the compute running, bypassing scale-to-zero.
  • Schema-only branches -- Use neonctl branches create --schema-only or the REST API with "init_source": "schema-only". Schema-only branches require exactly one read-write compute endpoint.
  • Branch history has a retention window -- Free plan: 6 hours. Launch: 7 days. Scale: 30 days. You cannot restore beyond this window.
  • Node.js v19+ required -- The GA version of @neondatabase/serverless (v1.0+) requires Node.js 19 or higher.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use the neon() HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)

(You MUST close Pool/Client connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)

(You MUST use pooled connection strings (-pooler suffix) for serverless workloads -- direct connections exhaust the limited connection slots)

(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)

(You MUST use sql.unsafe() only for trusted, known-safe strings like table/column names -- never for user input)

Failure to follow these rules will cause connection exhaustion, SQL injection vulnerabilities, or intermittent cold-start failures.

</critical_reminders>

Files (skills)
  • examples
    • branching.md 9.1 KB
      # Neon -- Branching Examples
      
      > Database branching for dev, preview, and CI environments. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Prerequisites:** Understand connection string setup and driver patterns from [core.md](core.md) first.
      
      ---
      
      ## Pattern 1: Dev Branch Workflow
      
      ### Good Example -- Feature Development Branch
      
      ```bash
      # Create a branch for a feature (copies schema + data from parent via copy-on-write)
      neonctl branches create --name dev/feat-user-profiles --project-id $NEON_PROJECT_ID
      
      # Get the branch's pooled connection string
      neonctl connection-string --project-id $NEON_PROJECT_ID --branch dev/feat-user-profiles --pooled
      
      # Run migrations against the branch
      DATABASE_URL=$(neonctl connection-string --project-id $NEON_PROJECT_ID --branch dev/feat-user-profiles) \
        npx your-migration-tool migrate
      
      # When done: reset branch to re-sync with production, or delete it
      neonctl branches reset dev/feat-user-profiles --parent --project-id $NEON_PROJECT_ID
      neonctl branches delete dev/feat-user-profiles --project-id $NEON_PROJECT_ID
      ```
      
      **Why good:** Branch naming convention (`dev/feat-*`) maps to git workflow, `--pooled` flag returns the pooled connection string, reset re-syncs with parent without recreating, clean deletion when feature is complete
      
      ---
      
      ## Pattern 2: PR Preview Branches with GitHub Actions
      
      ### Good Example -- Create Branch on PR Open, Delete on Close
      
      ```yaml
      # .github/workflows/preview-branch.yml
      name: Preview Database Branch
      
      on:
        pull_request:
          types: [opened, synchronize, closed]
      
      env:
        NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
        NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
      
      jobs:
        create-branch:
          if: github.event.action != 'closed'
          runs-on: ubuntu-latest
          outputs:
            db_url: ${{ steps.branch.outputs.db_url }}
          steps:
            - uses: neondatabase/create-branch-action@v6
              id: branch
              with:
                project_id: ${{ env.NEON_PROJECT_ID }}
                api_key: ${{ env.NEON_API_KEY }}
                branch_name: preview/pr-${{ github.event.number }}
                role: neondb_owner
      
            - name: Run migrations
              env:
                DATABASE_URL: ${{ steps.branch.outputs.db_url }}
              run: npx your-migration-tool migrate
      
            - name: Comment PR with branch info
              uses: actions/github-script@v7
              with:
                script: |
                  github.rest.issues.createComment({
                    issue_number: context.issue.number,
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    body: `Database branch \`preview/pr-${context.issue.number}\` created.`
                  })
      
        delete-branch:
          if: github.event.action == 'closed'
          runs-on: ubuntu-latest
          steps:
            - uses: neondatabase/delete-branch-action@v3
              with:
                project_id: ${{ env.NEON_PROJECT_ID }}
                api_key: ${{ env.NEON_API_KEY }}
                branch: preview/pr-${{ github.event.number }}
      ```
      
      **Why good:** Branch lifecycle tied to PR lifecycle (create on open, delete on close), `synchronize` event handles force pushes, migration runs against branch, consistent naming convention `preview/pr-{number}`, official Neon GitHub Actions for reliability
      
      ---
      
      ## Pattern 3: Programmatic Branch Management (Neon API)
      
      ### Good Example -- TypeScript Branch Manager for CI/CD
      
      ```typescript
      const NEON_API_BASE = "https://console.neon.tech/api/v2";
      
      interface NeonBranch {
        id: string;
        name: string;
        parent_id: string;
        created_at: string;
      }
      
      interface NeonEndpoint {
        host: string;
      }
      
      interface CreateBranchResponse {
        branch: NeonBranch;
        endpoints: NeonEndpoint[];
        connection_uris: Array<{ connection_uri: string }>;
      }
      
      async function neonApi<T>(
        path: string,
        apiKey: string,
        options: RequestInit = {},
      ): Promise<T> {
        const response = await fetch(`${NEON_API_BASE}${path}`, {
          ...options,
          headers: {
            Authorization: `Bearer ${apiKey}`,
            "Content-Type": "application/json",
            ...options.headers,
          },
        });
      
        if (!response.ok) {
          const body = await response.text();
          throw new Error(`Neon API error (${response.status}): ${body}`);
        }
      
        return response.json() as Promise<T>;
      }
      
      // Create a preview branch with auto-expiration
      const PREVIEW_BRANCH_TTL_DAYS = 7;
      
      async function createPreviewBranch(
        projectId: string,
        prNumber: number,
        apiKey: string,
      ): Promise<{ branchId: string; connectionUri: string }> {
        const expiresAt = new Date();
        expiresAt.setDate(expiresAt.getDate() + PREVIEW_BRANCH_TTL_DAYS);
      
        const result = await neonApi<CreateBranchResponse>(
          `/projects/${projectId}/branches`,
          apiKey,
          {
            method: "POST",
            body: JSON.stringify({
              branch: {
                name: `preview/pr-${prNumber}`,
                expires_at: expiresAt.toISOString(),
              },
              endpoints: [{ type: "read_write" }],
            }),
          },
        );
      
        return {
          branchId: result.branch.id,
          connectionUri: result.connection_uris[0].connection_uri,
        };
      }
      
      // Delete a preview branch
      async function deletePreviewBranch(
        projectId: string,
        branchId: string,
        apiKey: string,
      ): Promise<void> {
        await neonApi(`/projects/${projectId}/branches/${branchId}`, apiKey, {
          method: "DELETE",
        });
      }
      
      // List all branches (for cleanup scripts)
      async function listBranches(
        projectId: string,
        apiKey: string,
      ): Promise<NeonBranch[]> {
        const result = await neonApi<{ branches: NeonBranch[] }>(
          `/projects/${projectId}/branches`,
          apiKey,
        );
        return result.branches;
      }
      ```
      
      **Why good:** Generic `neonApi` helper with typed responses, TTL expiration ensures auto-cleanup even if delete fails, named constant for TTL, typed interfaces for API responses, error includes status code and body for debugging
      
      ---
      
      ## Pattern 4: Schema-Only Branches
      
      ### Good Example -- Branching Without Sensitive Data
      
      ```typescript
      // Schema-only branches copy structure but NOT data
      // Use for: CI testing with fixtures, sensitive data compliance
      
      const NEON_API_BASE = "https://console.neon.tech/api/v2";
      
      async function createSchemaOnlyBranch(
        projectId: string,
        branchName: string,
        apiKey: string,
      ): Promise<string> {
        const response = await fetch(
          `${NEON_API_BASE}/projects/${projectId}/branches`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${apiKey}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              branch: {
                name: branchName,
                init_source: "schema-only", // Key: copies DDL but not rows
              },
              endpoints: [{ type: "read_write" }],
            }),
          },
        );
      
        if (!response.ok) {
          throw new Error(
            `Failed to create schema-only branch: ${response.statusText}`,
          );
        }
      
        const result = await response.json();
        return result.branch.id;
      }
      ```
      
      **Why good:** `init_source: "schema-only"` creates branch with tables/indexes/constraints but no row data, useful for CI environments that seed their own test data, avoids copying sensitive production data
      
      **When to use:** When you need the schema but not production data (e.g., compliance requirements, CI test runs with fixtures, staging environments with synthetic data).
      
      ---
      
      ## Pattern 5: Branch Reset for Development
      
      ### Good Example -- Syncing Dev Branch with Production
      
      ```bash
      # Reset dev branch to match current state of parent (main)
      neonctl branches reset dev-alice --parent --project-id $NEON_PROJECT_ID
      
      # Reset with backup: saves current state under a new name before resetting
      neonctl branches reset dev-alice --parent \
        --preserve-under-name dev-alice-backup-$(date +%Y%m%d) \
        --project-id $NEON_PROJECT_ID
      ```
      
      **Why good:** `--parent` flag resets to parent's current state (not the state at branch creation), `--preserve-under-name` creates a backup in case you need to recover work, date-based backup naming prevents collisions
      
      **When to use:** After production schema changes that you want reflected in your dev branch, or when your dev data becomes too divergent from production to be useful.
      
      ---
      
      ## Pattern 6: Branch Cleanup Script
      
      ### Good Example -- Delete Stale Preview Branches
      
      ```typescript
      const STALE_BRANCH_THRESHOLD_DAYS = 14;
      const PREVIEW_BRANCH_PREFIX = "preview/pr-";
      
      async function cleanupStaleBranches(
        projectId: string,
        apiKey: string,
      ): Promise<string[]> {
        const branches = await listBranches(projectId, apiKey);
        const now = new Date();
        const deleted: string[] = [];
      
        for (const branch of branches) {
          // Only clean up preview branches
          if (!branch.name.startsWith(PREVIEW_BRANCH_PREFIX)) {
            continue;
          }
      
          const createdAt = new Date(branch.created_at);
          const ageInDays =
            (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24);
      
          if (ageInDays > STALE_BRANCH_THRESHOLD_DAYS) {
            await deletePreviewBranch(projectId, branch.id, apiKey);
            deleted.push(branch.name);
          }
        }
      
        return deleted;
      }
      ```
      
      **Why good:** Named constants for threshold and prefix, only targets preview branches (never dev or main), returns list of deleted branches for logging, safe to run repeatedly (idempotent for already-deleted branches)
      
      **When to use:** As a scheduled CI job (weekly cron) to clean up preview branches from closed PRs that were not properly deleted, or as a cost-control measure.
      
      ---
      
      _For driver setup and query patterns, see [core.md](core.md)._
      
    • core.md 11.3 KB
      # Neon -- Core Examples
      
      > Driver setup, HTTP queries, WebSocket connections, and transaction patterns. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Branching & CI/CD patterns:** See [branching.md](branching.md).
      
      ---
      
      ## Pattern 1: HTTP Query Function Setup
      
      ### Good Example -- Typed Query Client
      
      ```typescript
      // lib/db.ts
      import { neon } from "@neondatabase/serverless";
      
      const DATABASE_URL = process.env.DATABASE_URL!;
      
      // Create the SQL tagged template function
      export const sql = neon(DATABASE_URL);
      
      // Usage in a handler
      export async function getActiveUsers() {
        const ACTIVE_STATUS = "active";
        const users =
          await sql`SELECT id, name, email FROM users WHERE status = ${ACTIVE_STATUS}`;
        return users;
      }
      ```
      
      **Why good:** `neon()` returns a tagged template function that auto-parameterizes all interpolated values, named constant for status value, export enables reuse across handlers
      
      ### Bad Example -- String Concatenation
      
      ```typescript
      import { neon } from "@neondatabase/serverless";
      
      const sql = neon(DATABASE_URL);
      
      // BAD: sql.unsafe with user input -- SQL injection vulnerability
      async function searchUsers(userInput: string) {
        return await sql`SELECT * FROM ${sql.unsafe(userInput)}`; // user controls table name!
      }
      ```
      
      **Why bad:** `sql.unsafe()` inserts raw SQL without parameterization -- user input can inject arbitrary SQL. Only use `sql.unsafe()` for hardcoded, trusted identifiers.
      
      ### Good Example -- Dynamic SQL with `sql.query()`
      
      ```typescript
      import { neon } from "@neondatabase/serverless";
      
      const sql = neon(DATABASE_URL);
      
      // Use sql.query() when the SQL string is in a variable
      async function findByColumn(column: "email" | "username", value: string) {
        // Column name from trusted allowlist, user value parameterized via $1
        const q = `SELECT id, name FROM users WHERE ${column} = $1`;
        return await sql.query(q, [value]);
      }
      ```
      
      **Why good:** `sql.query()` accepts numbered placeholders (`$1`, `$2`) for safe parameterization when the query is a string variable rather than a template literal, column name from a typed allowlist (not user input)
      
      ---
      
      ## Pattern 2: Full Results with Metadata
      
      ### Good Example -- Row Count and Field Info
      
      ```typescript
      import { neon } from "@neondatabase/serverless";
      
      const sql = neon(DATABASE_URL, { fullResults: true });
      
      async function getPostsWithCount() {
        const result = await sql`SELECT id, title FROM posts WHERE published = true`;
      
        // result.rows -- the data rows
        // result.rowCount -- number of rows returned
        // result.fields -- column metadata (name, dataTypeID)
        // result.command -- "SELECT", "INSERT", etc.
      
        return {
          posts: result.rows,
          total: result.rowCount,
        };
      }
      ```
      
      **Why good:** `fullResults: true` provides metadata alongside data, useful for pagination and debugging without a separate COUNT query
      
      ---
      
      ## Pattern 3: Composable Query Fragments
      
      ### Good Example -- Dynamic Query Building
      
      ```typescript
      import { neon } from "@neondatabase/serverless";
      
      const sql = neon(DATABASE_URL);
      
      // Reusable filter fragments
      function buildFilters(options: { status?: string; authorId?: string }) {
        const conditions: ReturnType<typeof sql>[] = [];
      
        if (options.status) {
          conditions.push(sql`status = ${options.status}`);
        }
        if (options.authorId) {
          conditions.push(sql`author_id = ${options.authorId}`);
        }
      
        if (conditions.length === 0) {
          return sql`TRUE`;
        }
      
        // Compose with AND -- parameters renumber automatically
        return conditions.reduce((acc, condition) => sql`${acc} AND ${condition}`);
      }
      
      const PAGE_SIZE = 25;
      
      async function getPosts(options: {
        status?: string;
        authorId?: string;
        page: number;
      }) {
        const where = buildFilters(options);
        const offset = options.page * PAGE_SIZE;
      
        return sql`
          SELECT id, title, created_at
          FROM posts
          WHERE ${where}
          ORDER BY created_at DESC
          LIMIT ${PAGE_SIZE} OFFSET ${offset}
        `;
      }
      ```
      
      **Why good:** Fragments compose safely with automatic parameter renumbering, typed filter function, named constant for page size, no raw string concatenation
      
      ---
      
      ## Pattern 4: HTTP Transactions
      
      ### Good Example -- Atomic Balance Transfer
      
      ```typescript
      import { neon } from "@neondatabase/serverless";
      
      const sql = neon(DATABASE_URL);
      
      async function transferFunds(fromId: string, toId: string, amount: number) {
        const MIN_TRANSFER_AMOUNT = 0;
      
        if (amount <= MIN_TRANSFER_AMOUNT) {
          throw new Error("Transfer amount must be positive");
        }
      
        // All queries execute atomically in a single HTTP round trip
        const [debit, credit] = await sql.transaction(
          (txn) => [
            txn`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId} RETURNING balance`,
            txn`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId} RETURNING balance`,
          ],
          { isolationLevel: "Serializable" },
        );
      
        return { fromBalance: debit[0].balance, toBalance: credit[0].balance };
      }
      ```
      
      **Why good:** Serializable isolation prevents concurrent transfer races, function form for transaction, single HTTP round trip for both queries, RETURNING avoids a separate SELECT, named constant for validation
      
      ### Good Example -- Read-Only Transaction for Consistent Snapshots
      
      ```typescript
      const [posts, stats] = await sql.transaction(
        [
          sql`SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 10`,
          sql`SELECT count(*)::int AS total, count(*) FILTER (WHERE published)::int AS published FROM posts`,
        ],
        { readOnly: true, isolationLevel: "RepeatableRead" },
      );
      ```
      
      **Why good:** Read-only + RepeatableRead ensures consistent snapshot across both queries, array form is concise for predetermined queries
      
      ---
      
      ## Pattern 5: WebSocket Pool in Serverless Handler
      
      ### Good Example -- Request-Scoped Pool
      
      ```typescript
      import { Pool } from "@neondatabase/serverless";
      
      const DATABASE_URL = process.env.DATABASE_URL!;
      
      // Edge/serverless handler pattern
      export async function handleRequest(request: Request): Promise<Response> {
        const pool = new Pool({ connectionString: DATABASE_URL });
      
        try {
          const { rows } = await pool.query(
            "SELECT id, title, content FROM posts WHERE published = $1 ORDER BY created_at DESC LIMIT $2",
            [true, 10],
          );
      
          return new Response(JSON.stringify(rows), {
            headers: { "Content-Type": "application/json" },
          });
        } catch (error) {
          const message = error instanceof Error ? error.message : "Unknown error";
          return new Response(JSON.stringify({ error: message }), { status: 500 });
        } finally {
          // ctx.waitUntil(pool.end()) if available, otherwise await
          await pool.end();
        }
      }
      ```
      
      **Why good:** Pool created inside handler, pool.end() in finally block, parameterized query, error handling with proper types
      
      ### Good Example -- Using ctx.waitUntil for Non-Blocking Cleanup
      
      ```typescript
      // Serverless platform with ExecutionContext (e.g., edge workers)
      export default {
        async fetch(
          request: Request,
          env: Env,
          ctx: ExecutionContext,
        ): Promise<Response> {
          const pool = new Pool({ connectionString: env.DATABASE_URL });
      
          try {
            const { rows } = await pool.query("SELECT id, name FROM users LIMIT 10");
            return new Response(JSON.stringify(rows));
          } finally {
            // Close pool without blocking the response
            ctx.waitUntil(pool.end());
          }
        },
      };
      ```
      
      **Why good:** `ctx.waitUntil()` closes the pool after the response is sent, reducing latency for the client while still cleaning up connections
      
      ---
      
      ## Pattern 6: Interactive Transactions via WebSocket
      
      ### Good Example -- Multi-Step Transaction with Conditional Logic
      
      ```typescript
      import { Pool } from "@neondatabase/serverless";
      
      async function createOrderWithInventoryCheck(
        productId: string,
        quantity: number,
        userId: string,
      ): Promise<{ orderId: string }> {
        const pool = new Pool({ connectionString: process.env.DATABASE_URL });
      
        try {
          const client = await pool.connect();
          try {
            await client.query("BEGIN");
      
            // Check inventory (interactive -- depends on result)
            const {
              rows: [product],
            } = await client.query(
              "SELECT id, stock, price FROM products WHERE id = $1 FOR UPDATE",
              [productId],
            );
      
            if (!product || product.stock < quantity) {
              await client.query("ROLLBACK");
              throw new Error("Insufficient stock");
            }
      
            // Deduct inventory
            await client.query(
              "UPDATE products SET stock = stock - $1 WHERE id = $2",
              [quantity, productId],
            );
      
            // Create order
            const {
              rows: [order],
            } = await client.query(
              "INSERT INTO orders (user_id, product_id, quantity, total) VALUES ($1, $2, $3, $4) RETURNING id",
              [userId, productId, quantity, product.price * quantity],
            );
      
            await client.query("COMMIT");
            return { orderId: order.id };
          } catch (error) {
            await client.query("ROLLBACK");
            throw error;
          } finally {
            client.release();
          }
        } finally {
          await pool.end();
        }
      }
      ```
      
      **Why good:** `FOR UPDATE` locks the row preventing concurrent stock deductions, interactive transaction (second query depends on first result), proper ROLLBACK on both error and business logic failure, client.release() + pool.end() in finally blocks
      
      **When to use:** Interactive transactions where subsequent queries depend on results of earlier queries. For predetermined query batches, use `sql.transaction()` over HTTP instead.
      
      ---
      
      ## Pattern 7: Node.js WebSocket Configuration
      
      ### Good Example -- Configuring ws for Node.js
      
      ```typescript
      // Required for Node.js v21 and below
      import { Pool, neonConfig } from "@neondatabase/serverless";
      import ws from "ws";
      
      // Set before creating any Pool/Client
      neonConfig.webSocketConstructor = ws;
      
      // Now Pool/Client work normally
      const pool = new Pool({ connectionString: process.env.DATABASE_URL });
      const { rows } = await pool.query("SELECT now()");
      await pool.end();
      ```
      
      **Why good:** WebSocket constructor configured before any connection is created, only needed for Node.js <= v21 (v22+ has built-in WebSocket)
      
      **When to use:** When running `Pool`/`Client` (WebSocket mode) in Node.js versions before v22. Not needed for the `neon()` HTTP function (it uses fetch, not WebSockets).
      
      ---
      
      ## Pattern 8: Connection Timeout Handling
      
      ### Good Example -- Appropriate Timeouts for Cold Starts
      
      ```typescript
      import { neon } from "@neondatabase/serverless";
      
      const CONNECTION_TIMEOUT_MS = 15_000; // 15 seconds -- accommodates cold start + query
      
      const sql = neon(process.env.DATABASE_URL!, {
        fetchOptions: {
          signal: AbortSignal.timeout(CONNECTION_TIMEOUT_MS),
        },
      });
      
      // For Pool/Client connections
      import { Pool } from "@neondatabase/serverless";
      
      const POOL_CONNECTION_TIMEOUT_MS = 10_000;
      
      const pool = new Pool({
        connectionString: process.env.DATABASE_URL,
        connectionTimeoutMillis: POOL_CONNECTION_TIMEOUT_MS,
      });
      ```
      
      **Why good:** Named constants for all timeouts, 15-second HTTP timeout accommodates cold start (200-500ms) plus query execution, Pool has its own timeout config
      
      ### Bad Example -- Default Timeouts
      
      ```typescript
      // BAD: No timeout configuration
      const sql = neon(DATABASE_URL);
      const pool = new Pool({ connectionString: DATABASE_URL });
      
      // After 5 min idle, cold start adds 200-500ms
      // Default fetch timeout may be too short, causing intermittent failures
      ```
      
      **Why bad:** Default timeouts do not account for Neon's scale-to-zero cold start latency, leading to intermittent timeout errors after idle periods
      
      ---
      
      _For branching and CI/CD patterns, see [branching.md](branching.md)._
      
  • reference.md 5.4 KB
    # Neon Reference
    
    > Quick lookup tables, CLI commands, and connection configuration. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples.
    
    ---
    
    ## Connection String Format
    
    ```
    # Pooled (serverless / high-concurrency)
    postgresql://[user]:[password]@[endpoint-id]-pooler.[region].aws.neon.tech/[dbname]?sslmode=require
    
    # Direct (migrations / session features)
    postgresql://[user]:[password]@[endpoint-id].[region].aws.neon.tech/[dbname]?sslmode=require
    ```
    
    The only difference is the `-pooler` suffix on the endpoint ID.
    
    ---
    
    ## `neon()` Configuration Options
    
    | Option         | Default     | Description                                                     |
    | -------------- | ----------- | --------------------------------------------------------------- |
    | `arrayMode`    | `false`     | Return rows as arrays instead of objects                        |
    | `fullResults`  | `false`     | Include metadata (fields, rowCount, command) in result          |
    | `fetchOptions` | `{}`        | Custom fetch config (signal, priority, cache, etc.)             |
    | `authToken`    | `undefined` | JWT string or async function returning a JWT for Neon Authorize |
    
    ---
    
    ## `sql.transaction()` Options
    
    | Option           | Values                                                               | Default         |
    | ---------------- | -------------------------------------------------------------------- | --------------- |
    | `isolationLevel` | `ReadUncommitted`, `ReadCommitted`, `RepeatableRead`, `Serializable` | `ReadCommitted` |
    | `readOnly`       | `boolean`                                                            | `false`         |
    | `deferrable`     | `boolean` (only with `readOnly: true` + `Serializable`)              | `false`         |
    
    ---
    
    ## PgBouncer Pooling Settings (Non-Configurable)
    
    | Setting                   | Value                    |
    | ------------------------- | ------------------------ |
    | `pool_mode`               | `transaction`            |
    | `max_client_conn`         | 10,000                   |
    | `default_pool_size`       | 90% of `max_connections` |
    | `max_prepared_statements` | 1,000                    |
    | `query_wait_timeout`      | 120 seconds              |
    
    ---
    
    ## Connection Limits by Compute Size
    
    | Compute (CU) | `max_connections` | Notes                                       |
    | ------------ | ----------------- | ------------------------------------------- |
    | 0.25         | ~104              | Free plan default                           |
    | 1            | ~377              | Per-user-per-database pool in PgBouncer     |
    | 2            | ~753              |                                             |
    | 4            | ~1,507            |                                             |
    | 8            | ~3,014            |                                             |
    | 16           | ~4,000            | Maximum for scale-to-zero eligible computes |
    | 56           | ~4,000            | Always-on, Scale plan only                  |
    
    ---
    
    ## neonctl CLI Quick Reference
    
    ```bash
    # Install
    npm install -g neonctl
    
    # Authenticate
    neonctl auth
    
    # Branch management
    neonctl branches list --project-id <pid>
    neonctl branches create --name <name> --project-id <pid>
    neonctl branches create --name <name> --project-id <pid> --expires-at "2025-04-01T00:00:00Z"
    neonctl branches delete <name> --project-id <pid>
    neonctl branches reset <name> --parent --project-id <pid>
    neonctl branches restore <target> <source@timestamp> --project-id <pid>
    
    # Connection string
    neonctl connection-string --project-id <pid> --branch-id <bid>
    neonctl connection-string --project-id <pid> --branch-id <bid> --pooled
    ```
    
    **Authentication:** Use `--api-key` flag or `NEON_API_KEY` environment variable.
    
    ---
    
    ## Neon REST API Endpoints
    
    | Operation      | Method   | Endpoint                                              |
    | -------------- | -------- | ----------------------------------------------------- |
    | List branches  | `GET`    | `/projects/{project_id}/branches`                     |
    | Create branch  | `POST`   | `/projects/{project_id}/branches`                     |
    | Delete branch  | `DELETE` | `/projects/{project_id}/branches/{branch_id}`         |
    | Restore branch | `POST`   | `/projects/{project_id}/branches/{branch_id}/restore` |
    
    **Base URL:** `https://console.neon.tech/api/v2`
    
    **Auth header:** `Authorization: Bearer <NEON_API_KEY>`
    
    ---
    
    ## Branch History Retention
    
    | Plan   | Retention Window |
    | ------ | ---------------- |
    | Free   | 6 hours          |
    | Launch | 7 days           |
    | Scale  | 30 days          |
    
    ---
    
    ## Scale-to-Zero Quick Reference
    
    | Setting              | Default   | Configurable?                       |
    | -------------------- | --------- | ----------------------------------- |
    | Auto-suspend timeout | 5 minutes | Paid plans: up to 7 days or disable |
    | Cold start latency   | 200-500ms | Not configurable                    |
    | Max CU for suspend   | 16 CU     | Computes > 16 CU are always-on      |
    
    ---
    
    ## Environment Variables
    
    ```bash
    # Application
    DATABASE_URL=postgresql://...@ep-cool-dawn-123456-pooler.region.aws.neon.tech/dbname?sslmode=require
    DIRECT_DATABASE_URL=postgresql://...@ep-cool-dawn-123456.region.aws.neon.tech/dbname?sslmode=require
    
    # CI / Automation
    NEON_API_KEY=neon_api_...
    NEON_PROJECT_ID=your-project-id
    ```
    
    ---
    
    ## PostgreSQL Direct SSL Optimization
    
    PostgreSQL 17+ supports direct SSL negotiation, reducing connection time by ~119ms:
    
    ```
    DATABASE_URL=postgresql://...?sslmode=require&sslnegotiation=direct
    ```
    
  • SKILL.md 21.1 KB
    ---
    name: api-baas-neon
    description: Serverless PostgreSQL with branching, autoscaling, and edge-compatible driver
    ---
    
    # Neon Serverless PostgreSQL Patterns
    
    > **Quick Guide:** Use `@neondatabase/serverless` for edge/serverless database access. Prefer the `neon()` HTTP function for single queries (faster, stateless) and `Pool`/`Client` for interactive transactions. Use pooled connection strings (`-pooler` suffix) for serverless workloads, direct connections only for migrations. Branch your database for dev/preview environments using copy-on-write semantics. Always handle cold starts from scale-to-zero (200-500ms wake-up).
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use the `neon()` HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)**
    
    **(You MUST close `Pool`/`Client` connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)**
    
    **(You MUST use pooled connection strings (`-pooler` suffix) for serverless workloads -- direct connections exhaust the limited connection slots)**
    
    **(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)**
    
    **(You MUST use `sql.unsafe()` only for trusted, known-safe strings like table/column names -- never for user input)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Neon, @neondatabase/serverless, neon(), neonConfig, neon serverless driver, neon database, neon branch, neonctl, neon connection pooling, neon scale-to-zero, neon autoscaling, neon postgres, ep-\*-pooler
    
    **When to use:**
    
    - Querying Postgres from edge/serverless functions (edge runtimes, serverless platforms)
    - Setting up connection strings (pooled vs direct) for different workloads
    - Creating database branches for dev, preview, or CI environments
    - Managing scale-to-zero behavior and cold start optimization
    - Running transactions in serverless contexts (HTTP batch or WebSocket)
    - Programmatic branch management via Neon API or neonctl CLI
    
    **Key patterns covered:**
    
    - `neon()` HTTP queries with SQL tagged templates and composable fragments
    - `Pool`/`Client` WebSocket connections with proper lifecycle management
    - Pooled (`-pooler`) vs direct connection strings and when to use each
    - Database branching (dev branches, PR preview branches, schema-only branches)
    - Scale-to-zero behavior, cold start mitigation, and autoscaling
    - `sql.transaction()` for non-interactive HTTP transactions
    - Neon API and neonctl CLI for programmatic branch management
    
    **When NOT to use:**
    
    - Traditional long-lived server connections (use standard `pg` driver with TCP)
    - Complex ORM-specific patterns (use your ORM's own skill)
    - General PostgreSQL query syntax (use a SQL/Postgres skill)
    
    **Detailed Resources:**
    
    - For decision frameworks and quick lookup tables, see [reference.md](reference.md)
    
    **Driver & Queries:**
    
    - [examples/core.md](examples/core.md) -- Driver setup, HTTP queries, WebSocket connections, transactions
    
    **Branching & Operations:**
    
    - [examples/branching.md](examples/branching.md) -- Dev branches, PR previews, neonctl CLI, Neon API, CI/CD workflows
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Neon separates storage and compute for PostgreSQL, enabling serverless features impossible with traditional Postgres: scale-to-zero, instant branching, and autoscaling. The `@neondatabase/serverless` driver replaces TCP with HTTP and WebSockets, making Postgres accessible from edge runtimes that lack TCP support.
    
    **Core principles:**
    
    1. **HTTP for speed, WebSocket for sessions** -- The `neon()` function uses HTTP fetch (~3 round trips) for single queries. `Pool`/`Client` use WebSockets (~8 round trips) when you need sessions or interactive transactions. Pick the right transport for the job.
    2. **Pooled by default** -- Pooled connections route through PgBouncer (transaction mode), handling up to 10,000 concurrent clients. Direct connections are limited by compute size (100-4,000) and should only be used for migrations or features requiring session state.
    3. **Branches are cheap** -- Copy-on-write means a branch of a 500GB database allocates no extra storage until data diverges. Use branches freely for dev, preview, testing, and CI.
    4. **Scale-to-zero is the default** -- Computes suspend after 5 minutes of inactivity. Cold starts take 200-500ms. Design for this with timeouts, retries, and connection pooling.
    5. **SQL injection safety built in** -- The tagged template function parameterizes automatically. Since v1.0, calling `neon()` as a regular function is a type error, preventing accidental injection.
    
    **When to use Neon serverless driver:**
    
    - Edge/serverless functions that cannot open TCP connections
    - Applications benefiting from database branching (preview environments per PR)
    - Cost-sensitive workloads that benefit from scale-to-zero
    - High-concurrency serverless apps needing connection pooling
    
    **When NOT to use:**
    
    - Long-running server processes with persistent connections (use standard `pg` over TCP)
    - Workloads requiring session-level features through PgBouncer (LISTEN/NOTIFY, SET, temporary tables)
    - Databases larger than 16 CU that need always-on compute (scale-to-zero not available above 16 CU)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: HTTP Queries with `neon()`
    
    The `neon()` function creates an HTTP-based query function using SQL tagged templates. It is the fastest path for single, non-interactive queries.
    
    ```typescript
    import { neon } from "@neondatabase/serverless";
    
    const DATABASE_URL = process.env.DATABASE_URL!;
    const sql = neon(DATABASE_URL);
    
    // Tagged template -- parameters are auto-parameterized (safe from injection)
    const userId = "abc-123";
    const posts =
      await sql`SELECT id, title FROM posts WHERE author_id = ${userId}`;
    ```
    
    **Why good:** Tagged template auto-parameterizes values preventing SQL injection, HTTP is ~3 round trips vs ~8 for WebSocket, stateless (no connection to manage)
    
    ```typescript
    // BAD: Calling neon() as a regular function (v1.0+ type error)
    const sql = neon(DATABASE_URL);
    const result = await sql(`SELECT * FROM posts WHERE id = ${id}`); // TYPE ERROR + SQL injection risk
    ```
    
    **Why bad:** Since v1.0, calling the query function as a regular function (not a tagged template) is a runtime and type error -- this was changed specifically to prevent SQL injection from string interpolation
    
    #### `sql.query()` for Dynamic SQL
    
    When the query string is in a variable (not a template literal), use the `.query()` method with numbered placeholders:
    
    ```typescript
    const q = "SELECT * FROM posts WHERE id = $1 AND status = $2";
    const posts = await sql.query(q, [postId, "published"]);
    ```
    
    **When to use:** Dynamic SQL strings built at runtime, or queries stored in variables. Parameters are still safely parameterized via `$1`, `$2`, etc.
    
    ---
    
    ### Pattern 2: Composable Query Fragments
    
    Template queries support composition with automatic parameter numbering across fragments.
    
    ```typescript
    import { neon } from "@neondatabase/serverless";
    
    const sql = neon(DATABASE_URL);
    
    // Build queries from reusable fragments
    const whereClause = sql`WHERE status = ${"active"} AND role = ${"admin"}`;
    const orderClause = sql`ORDER BY created_at DESC`;
    const PAGE_SIZE = 20;
    const limitClause = sql`LIMIT ${PAGE_SIZE}`;
    
    const users =
      await sql`SELECT id, name, email FROM users ${whereClause} ${orderClause} ${limitClause}`;
    ```
    
    **Why good:** Parameters are renumbered automatically across composed fragments, named constant for page size, fragments are reusable across queries
    
    #### Dynamic Table/Column Names with `sql.unsafe()`
    
    ```typescript
    // ONLY for trusted, known-safe values -- never user input
    const TABLE_NAME = "posts";
    const results =
      await sql`SELECT * FROM ${sql.unsafe(TABLE_NAME)} WHERE id = ${postId}`;
    ```
    
    **When to use:** Only when you need to interpolate trusted identifiers (table names, column names) that cannot be parameterized in SQL. Never pass user-supplied values to `sql.unsafe()`.
    
    ---
    
    ### Pattern 3: HTTP Transactions with `sql.transaction()`
    
    Execute multiple queries atomically via HTTP without needing a WebSocket connection.
    
    ```typescript
    import { neon } from "@neondatabase/serverless";
    
    const sql = neon(DATABASE_URL);
    
    // Array form -- all queries execute in a single HTTP round trip
    const [posts, totalCount] = await sql.transaction([
      sql`SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 10`,
      sql`SELECT count(*) FROM posts`,
    ]);
    
    // Function form with transaction options
    const [, transferResult] = await sql.transaction(
      (txn) => [
        txn`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`,
        txn`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`,
      ],
      { isolationLevel: "Serializable" },
    );
    ```
    
    **Why good:** Atomic execution without WebSocket overhead, array form is concise for read-only batches, function form supports transaction options, isolation level controls consistency guarantees
    
    ```typescript
    // BAD: Running related queries as separate HTTP requests
    const posts = await sql`SELECT * FROM posts WHERE author_id = ${authorId}`;
    const author = await sql`SELECT * FROM users WHERE id = ${authorId}`;
    // Two separate HTTP round trips, not atomic, race conditions possible
    ```
    
    **Why bad:** Separate HTTP calls are not atomic, data can change between queries, double the network latency
    
    #### Transaction Options
    
    - `isolationLevel`: `ReadUncommitted` | `ReadCommitted` | `RepeatableRead` | `Serializable`
    - `readOnly`: boolean (default `false`)
    - `deferrable`: boolean (default `false`, only effective with `readOnly: true` + `Serializable`)
    
    ---
    
    ### Pattern 4: WebSocket Connections with `Pool`/`Client`
    
    Use `Pool` and `Client` for interactive transactions or `node-postgres` API compatibility. In serverless environments, connections must be created and closed within a single request handler.
    
    ```typescript
    import { Pool } from "@neondatabase/serverless";
    
    export async function handleRequest(request: Request): Promise<Response> {
      const pool = new Pool({ connectionString: process.env.DATABASE_URL });
      try {
        // ... queries ...
      } finally {
        await pool.end(); // ALWAYS close within the same request
      }
    }
    ```
    
    **Key rule:** Create Pool inside the handler, close with `pool.end()` in `finally`. A global Pool leaks WebSocket connections between serverless invocations.
    
    See [examples/core.md](examples/core.md) -- Patterns 5-7 for request-scoped pools, interactive transactions, and Node.js WebSocket configuration.
    
    ---
    
    ### Pattern 5: Connection String Setup
    
    Neon provides two connection string formats: pooled (via PgBouncer) and direct.
    
    ```bash
    # Pooled connection (note: -pooler suffix on endpoint ID)
    # Use for: serverless functions, web apps, high-concurrency workloads
    DATABASE_URL=postgresql://user:pass@ep-cool-dawn-123456-pooler.us-east-2.aws.neon.tech/dbname?sslmode=require
    
    # Direct connection (no -pooler suffix)
    # Use for: migrations, pg_dump, LISTEN/NOTIFY, session-level features
    DIRECT_DATABASE_URL=postgresql://user:pass@ep-cool-dawn-123456.us-east-2.aws.neon.tech/dbname?sslmode=require
    ```
    
    **Why good:** Pooled handles up to 10,000 concurrent clients via PgBouncer in transaction mode, direct provides full session features for admin tasks, separate env vars make the distinction explicit
    
    #### PgBouncer Transaction Mode Limitations
    
    The pooled connection runs PgBouncer in **transaction mode**, which means connections return to the pool after each transaction. This prohibits:
    
    - `SET` / `RESET` statements (use `ALTER ROLE ... SET` instead)
    - `LISTEN` / `NOTIFY`
    - `WITH HOLD CURSOR`
    - SQL-level `PREPARE` / `DEALLOCATE` (protocol-level prepared statements up to 1,000 are supported)
    - Temporary tables with `PRESERVE` / `DELETE ROWS`
    - Session-level advisory locks
    
    ---
    
    ### Pattern 6: Cold Start and Scale-to-Zero Handling
    
    Neon computes auto-suspend after 5 minutes of inactivity (default). Cold starts take 200-500ms. Design for this with appropriate timeouts and retry logic.
    
    ```typescript
    const CONNECTION_TIMEOUT_MS = 10_000;
    const sql = neon(process.env.DATABASE_URL!, {
      fetchOptions: { signal: AbortSignal.timeout(CONNECTION_TIMEOUT_MS) },
    });
    ```
    
    **Key rule:** Set explicit timeouts (10-15s) to accommodate cold start latency. Use exponential backoff retries for reliability.
    
    See [examples/core.md](examples/core.md) -- Pattern 8 for full timeout configuration and retry patterns.
    
    #### Scale-to-Zero Facts
    
    - Default auto-suspend: 5 minutes of inactivity
    - Cold start latency: ~200-500ms (varies by region and compute size)
    - Only available for computes up to 16 CU (larger computes stay always-on)
    - Paid plans can adjust suspend timeout (up to 7 days) or disable entirely
    - Active logical replication subscribers prevent suspension
    
    ---
    
    ### Pattern 7: Database Branching
    
    Neon branches use copy-on-write semantics -- branching a 500GB database is instant and allocates no extra storage until data diverges.
    
    ```bash
    # Install neonctl
    npm install -g neonctl
    
    # Create a dev branch from main
    neonctl branches create --name dev-alice --project-id <project-id>
    
    # Create a branch with automatic expiration (for CI/preview)
    neonctl branches create --name preview/pr-42 --project-id <project-id> --expires-at "2025-04-01T00:00:00Z"
    
    # Create schema-only branch (no data copied -- for sensitive environments)
    neonctl branches create --name ci-test --schema-only --project-id <project-id>
    
    # Reset a dev branch to match current production
    neonctl branches reset dev-alice --parent --project-id <project-id>
    
    # Delete a branch
    neonctl branches delete preview/pr-42 --project-id <project-id>
    ```
    
    **Why good:** Named branches map to git workflow, TTL expiration auto-cleans CI branches, schema-only branching protects sensitive data, reset syncs dev with production without recreating
    
    #### Branch Connection Strings
    
    Each branch gets its own endpoint. The branch connection string follows the same format but with a different endpoint ID:
    
    ```bash
    # Main branch
    postgresql://user:pass@ep-cool-dawn-123456-pooler.us-east-2.aws.neon.tech/dbname
    
    # Dev branch -- different endpoint ID
    postgresql://user:pass@ep-quiet-hill-789012-pooler.us-east-2.aws.neon.tech/dbname
    ```
    
    ---
    
    ### Pattern 8: Neon API for Programmatic Branch Management
    
    The Neon REST API (`https://console.neon.tech/api/v2`) enables programmatic branch management. Authenticate with `Authorization: Bearer <NEON_API_KEY>`. Key operations: create branches with TTL expiration, delete branches, list branches for cleanup scripts.
    
    ```typescript
    const NEON_API_BASE = "https://console.neon.tech/api/v2";
    // POST /projects/{projectId}/branches -- create with { branch: { name, expires_at }, endpoints: [{ type: "read_write" }] }
    // DELETE /projects/{projectId}/branches/{branchId} -- delete a branch
    ```
    
    See [examples/branching.md](examples/branching.md) -- Pattern 3 for a full typed TypeScript branch manager with create, delete, and list operations.
    See [reference.md](reference.md) for the complete API endpoint table.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### HTTP (`neon()`) vs WebSocket (`Pool`/`Client`)
    
    ```
    What kind of database operation?
    +-- Single query (SELECT, INSERT, UPDATE, DELETE)
    |   +-- YES --> Use neon() HTTP function (fastest, ~3 round trips)
    +-- Multiple queries that must be atomic?
    |   +-- Can all queries be determined upfront (non-interactive)?
    |   |   +-- YES --> Use sql.transaction() over HTTP
    |   |   +-- NO --> Use Pool/Client over WebSocket
    +-- Need node-postgres (pg) API compatibility?
    |   +-- YES --> Use Pool/Client over WebSocket
    +-- Running in edge runtime (no TCP)?
        +-- YES --> Use @neondatabase/serverless (HTTP or WebSocket)
        +-- NO --> Standard pg driver with TCP may be simpler
    ```
    
    ### Pooled vs Direct Connection
    
    ```
    What is the workload?
    +-- Serverless function / edge function --> Pooled (-pooler)
    +-- Web application (many concurrent requests) --> Pooled (-pooler)
    +-- Schema migration --> Direct (needs session state)
    +-- pg_dump / pg_restore --> Direct (uses SET statements)
    +-- LISTEN / NOTIFY --> Direct (session-level feature)
    +-- Long-running analytics query --> Direct (avoid pool contention)
    +-- Default / unsure --> Pooled (-pooler)
    ```
    
    ### Branch Strategy
    
    ```
    What do you need the branch for?
    +-- Developer working on a feature --> Dev branch (long-lived, manually managed)
    +-- PR preview environment --> Preview branch (TTL expiration, auto-cleanup on merge)
    +-- CI test run --> Ephemeral branch (short TTL, schema-only if data-sensitive)
    +-- Database recovery --> Restore from branch history (up to 30 days on Scale plan)
    +-- Load testing --> Branch from production (copy-on-write, no storage cost until diverge)
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - **Global `Pool` in serverless** -- Creating a Pool outside the request handler in edge/serverless functions leaks WebSocket connections. Pool/Client must be created, used, and closed within a single request.
    - **Using direct connection string in serverless** -- Direct connections bypass PgBouncer and are limited to compute-size max connections (100-4,000). Serverless functions should always use pooled (`-pooler`) connections.
    - **Passing user input to `sql.unsafe()`** -- `sql.unsafe()` embeds raw SQL without parameterization. It exists only for trusted identifiers (table/column names). User input in `sql.unsafe()` is a SQL injection vulnerability.
    
    **Medium Priority Issues:**
    
    - **Double pooling** -- Combining Neon's server-side PgBouncer with a client-side connection pool in your driver creates unnecessary overhead. Let Neon handle pooling.
    - **Ignoring `pool.end()` in serverless** -- Forgetting to call `pool.end()` after using WebSocket connections exhausts available connections across invocations.
    - **Using `SET` statements through pooled connections** -- PgBouncer transaction mode resets session state after each transaction. Use `ALTER ROLE ... SET` for role-level defaults or use direct connections.
    - **Not handling cold start latency** -- First request after idle period adds 200-500ms. Without appropriate timeouts (10+ seconds) and retry logic, applications fail intermittently.
    
    **Common Mistakes:**
    
    - **Wrong package name** -- The package is `@neondatabase/serverless`, not `neon-serverless` or `pg-neon`.
    - **Missing `ws` package on Node.js <= v21** -- Node.js versions before v22 lack built-in WebSocket support. When using `Pool`/`Client`, install `ws` and set `neonConfig.webSocketConstructor = ws`. Node.js v22+ has native WebSocket and needs no extra setup.
    - **Calling `neon()` result as a function instead of tagged template** -- `sql("SELECT ...")` is a type error since v1.0. Use `` sql`SELECT ...` `` (tagged template).
    - **Expecting Pool to survive across serverless invocations** -- Each cold start creates a new execution context. Do not rely on global state for connection management.
    - **64MB request/response limit** -- HTTP mode has a 64MB payload limit. Large result sets or bulk inserts must be chunked.
    
    **Gotchas & Edge Cases:**
    
    - **Transaction options apply to the transaction, not individual queries** -- Setting `arrayMode: true` on individual queries inside `sql.transaction()` is ignored. Set it on the transaction itself.
    - **PgBouncer's 120-second query wait timeout** -- If all pooled connections are busy, new queries queue for up to 120 seconds before timing out.
    - **Branch endpoints are different from parent** -- Each branch gets a unique endpoint ID. You cannot use the parent's connection string to connect to a child branch.
    - **Scale-to-zero only for computes <= 16 CU** -- Computes larger than 16 CU remain always-on regardless of configuration.
    - **Logical replication prevents suspension** -- Active replication subscribers keep the compute running, bypassing scale-to-zero.
    - **Schema-only branches** -- Use `neonctl branches create --schema-only` or the REST API with `"init_source": "schema-only"`. Schema-only branches require exactly one read-write compute endpoint.
    - **Branch history has a retention window** -- Free plan: 6 hours. Launch: 7 days. Scale: 30 days. You cannot restore beyond this window.
    - **Node.js v19+ required** -- The GA version of `@neondatabase/serverless` (v1.0+) requires Node.js 19 or higher.
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use the `neon()` HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)**
    
    **(You MUST close `Pool`/`Client` connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)**
    
    **(You MUST use pooled connection strings (`-pooler` suffix) for serverless workloads -- direct connections exhaust the limited connection slots)**
    
    **(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)**
    
    **(You MUST use `sql.unsafe()` only for trusted, known-safe strings like table/column names -- never for user input)**
    
    **Failure to follow these rules will cause connection exhaustion, SQL injection vulnerabilities, or intermittent cold-start failures.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related