Serverless Postgres skill for Codex
Pooled at runtime, direct for migrations, branched per PR. Codex wires serverless apps to Neon or Supabase with two connection strings: a transaction-mode pooled URL for runtime and a direct URL for migrations. It picks a driver that fits the runtime, turns off prepared statements where the pooler cannot handle them, sizes timeouts for scale-to-zero wake-ups, and sets up a database branch per pull request without copying unmasked production data into previews.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- serverless-postgres Installs to ~/.agents/skills/serverless-postgres/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install serverless-postgres The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project serverless-postgres Installs to .agents/skills/serverless-postgres/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/serverless-postgres.zipunzip -o serverless-postgres.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) serverless-postgres PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $serverless-postgres, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Runs applications on serverless and managed Postgres such as Neon and Supabase - pooled versus direct connection strings, transaction-mode pooler limits on prepared statements and session state, driver choice for Lambda, Vercel and edge runtimes including the Neon serverless HTTP and WebSocket driver, scale-to-zero cold starts and timeouts, database branching for preview environments and CI, and safe migrations through the direct URL. Use when connecting an app on Vercel, Lambda, Cloudflare Workers or similar to Neon or Supabase Postgres, seeing too many connections or prepared statement errors, slow first requests after idle, or setting up a branch per pull request.
Use it when
- Connecting serverless or edge functions to Neon or Supabase Postgres
- Errors about connection limits or prepared statements behind a pooler
- First requests after idle are slow or time out
- Setting up per-PR or per-test database branches
Use something else when
- Self-hosted PgBouncer setup and pool sizing math: use connection-pooling
- Supabase auth, storage and policies: use supabase-backend
How to check the result
A burst of concurrent requests against a preview shows no connection or prepared statement errors, the first request after idle finishes within the timeout, and CI migrates and deletes each PR branch.
The mistake it prevents
Pointing migrations at the transaction-mode pooled URL, where session advisory locks fail and the migration tool hangs.
Done well means: runtime traffic goes through a pooler with a driver configured for it, migrations and session-dependent work use the direct connection, cold starts are either removed for production or absorbed by timeouts and retries, and every preview or CI run gets its own disposable branch that never contains unmasked production personal data.
Inspect first
- Read
AGENTS.md; find the deploy target (Vercel, Netlify, AWS Lambda, Cloudflare Workers, containers) and runtime (Node, edge/V8 isolate, Deno, Bun). - Find the provider from connection strings in
.env.exampleor config (never print real secrets): Neon hosts end inneon.tech(pooled hosts contain-pooler); Supabase pooler hosts look like*.pooler.supabase.com, direct hostsdb.<ref>.supabase.co. - Find the driver and ORM (
pg,postgres,@neondatabase/serverless, Prisma, Drizzle, Kysely, SQLAlchemy) and how many clients each instance creates. - Check whether migrations run in CI, at build, or at app start, and which URL they use.
Connection strings
| Use | Neon | Supabase |
|---|---|---|
| App runtime in serverless functions | Pooled host (-pooler), PgBouncer transaction mode | Supavisor transaction mode, port 6543 |
| Long-lived server needing session features | Direct host | Supavisor session mode (port 5432 on pooler host) or direct |
Migrations, pg_dump, LISTEN, advisory locks held across statements | Direct host | Direct host (IPv6 by default; session pooler if IPv4 only) |
| Edge runtimes without TCP | @neondatabase/serverless over HTTP/WebSocket | supabase-js Data API, or an HTTP-capable driver |
Keep two variables: DATABASE_URL (pooled, runtime) and DIRECT_URL (or
DATABASE_URL_UNPOOLED) for migrations. Point ORM migration config at the direct one
(Prisma 7: datasource.url in prisma.config.ts; Drizzle: dbCredentials.url in
drizzle.config.ts).
Transaction-mode pooler rules
In transaction mode a client gets a server connection only for one transaction, so anything that assumes the same session across statements breaks:
SETwithoutLOCAL, session-level advisory locks, temp tables,LISTEN/NOTIFY, andWITH HOLDcursors do not work reliably. UseSET LOCAL/set_config(..., true)inside a transaction (this matters for RLS tenant context).- Named prepared statements: supported only if the pooler is configured for them (PgBouncer
1.21+ with
max_prepared_statements). If you seeprepared statement "s1" already existsordoes not exist, disable them in the driver:
| Driver | Setting |
|---|---|
| postgres.js | postgres(url, { prepare: false }) |
| asyncpg / SQLAlchemy | statement_cache_size=0 and prepared_statement_cache_size=0 |
| Prisma 6 (built-in engine) | ?pgbouncer=true on the pooled URL |
node-postgres (pg) | Unnamed statements by default; avoid passing name in queries |
Pooler and pool sizing in depth: $connection-pooling.
Drivers per runtime
- Node serverless (Lambda, Vercel Node functions):
pgor postgres.js against the pooled URL. Create the client outside the handler so warm invocations reuse it; keep the in-process pool small (max: 1-5) because each instance has its own pool. - Edge / Workers:
@neondatabase/serverless:
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`select id, name from projects where org_id = ${orgId} limit 20`;
The neon() HTTP function suits single queries and non-interactive transactions
(sql.transaction([...])). For interactive transactions or session features, use its
Pool/Client over WebSockets, created and closed within the request on edge runtimes.
- Cloudflare Workers with TCP: Hyperdrive in front of Postgres pools and caches
connections; use it with
pgor postgres.js when available to the project. - Containers / long-lived servers: standard pool, direct or session-mode URL; the serverless constraints above do not apply.
Cold starts and timeouts
- Neon computes scale to zero after a period of inactivity (5 minutes by default); the first query then waits for the compute to start, typically a few hundred milliseconds, sometimes more. Supabase free projects pause after longer inactivity and need a manual resume.
- Production: disable scale-to-zero or raise the suspend timeout if latency matters (plan dependent); keep it on for previews and dev branches to save cost.
- Set connect timeouts to tolerate a wake-up (for example 10-15 s for
pgconnectionTimeoutMillis), and retry connection errors once with jitter. Do not retry non-idempotent writes blindly. - Do not “keep warm” with cron pings in production as a substitute for configuration; it costs compute and hides the setting.
Branching for previews and CI
Neon branches are copy-on-write copies of a parent at a point in time.
neonctl branches create --name preview/pr-123 --parent main # needs network + NEON_API_KEY
neonctl connection-string preview/pr-123 --pooled
neonctl branches reset preview/pr-123 --parent # refresh from parent (discards branch changes)
neonctl branches delete preview/pr-123
In GitHub Actions, Neon publishes create/delete branch actions; pin them to a current
major version and pass the branch URL to migrations and the preview deploy. Supabase
branching creates preview projects per Git branch through its GitHub integration, running
supabase/migrations and supabase/seed.sql.
Rules:
- Run migrations against the branch in CI before deploying the preview; a failing migration fails the PR.
- Delete branches when PRs close; orphaned branches cost storage and compute.
- A branch of production contains production data. Branch from a masked or seeded parent for previews accessible to more people; never expose real customer data in preview apps.
branches reset,branches delete, and anything touching the production branch require explicit user confirmation naming the branch.
Migrations
- Run through the direct URL, from one place (CI job or release step), never from every function instance at cold start.
- Set
lock_timeout(for exampleSET lock_timeout = '5s') in migrations so a blocked DDL fails fast instead of queueing every request behind it. - Large-table changes follow expand/contract (
$zero-downtime-migrations).
Gotchas
- Using the pooled URL for migrations: tools that rely on session advisory locks (Prisma Migrate, many others) hang or fail.
- Creating a new client per request in Node functions exhausts connections under burst traffic.
- Supabase direct connections are IPv6; IPv4-only platforms need the session pooler or the IPv4 add-on.
- Provider connection limits scale with compute size; check them before raising pool sizes.
- Copying a production connection string into
.envfiles committed to the repo; keep real URLs in the platform’s secret store.
Verification
- Load test or burst (for example 100 concurrent requests) against a preview: no
too many connectionsor prepared statement errors. - First request after idle completes within the configured timeout; measure it.
- CI log shows migrations applied to the PR branch and the branch deleted on close.
select count(*) from pg_stat_activityon the direct connection stays within limits under load.
Output
**Provider/runtime:** <Neon | Supabase> on <Vercel | Lambda | Workers | ...>
**URLs:** runtime=<pooled, mode>, migrations=<direct>
**Driver config:** <driver, pool max, prepare setting, timeouts>
**Cold start policy:** <scale-to-zero on/off per environment, measured wake time>
**Branching:** <tool, naming, data source (masked/seeded), cleanup> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Serverless Postgres"
short_description: "Pooled at runtime, direct for migrations, branched per PR"
default_prompt: "$serverless-postgres Our Vercel functions hit too many connections on Neon, fix the connection setup" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.