Serverless PostgreSQL with branching, autoscaling, and edge-compatible driver
Scanned 5/27/2026
Install via CLI
openskills install agents-inc/skills---
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>
No comments yet. Be the first to comment!