api-baas-neon
Serverless PostgreSQL with branching, autoscaling, and edge-compatible driver
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-baas-neon/skills/api-baas-neon
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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/serverlessfor edge/serverless database access. Prefer theneon()HTTP function for single queries (faster, stateless) andPool/Clientfor interactive transactions. Use pooled connection strings (-poolersuffix) 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 fragmentsPool/ClientWebSocket 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
pgdriver 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:
- examples/branching.md -- Dev branches, PR previews, neonctl CLI, Neon API, CI/CD workflows
<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
Poolin 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 insql.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 callpool.end()after using WebSocket connections exhausts available connections across invocations. - Using
SETstatements through pooled connections -- PgBouncer transaction mode resets session state after each transaction. UseALTER ROLE ... SETfor 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, notneon-serverlessorpg-neon. - Missing
wspackage on Node.js <= v21 -- Node.js versions before v22 lack built-in WebSocket support. When usingPool/Client, installwsand setneonConfig.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. Usesql`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: trueon individual queries insidesql.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-onlyor 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.
Reviews (0)
No reviews yet.
No comments yet.