mcpbeat Sign in

Neon Skill for Cursor

Use when you have picked Neon (serverless Postgres) and need to connect correctly from a serverless or edge runtime, wire database branching into dev, preview and CI, or stop being burned by scale-to-zero cold starts and pooler limits — including choosing HTTP versus WebSocket connections and pooled versus direct endpoints for migrations. NOT generic Postgres schema, index, EXPLAIN, RLS or migration-engine work (that is `postgresdb`).

6k tokens
context cost
the whole folder, loaded on every use
5
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill neon

What comes with it

10 590 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/branching-ci.md
scripts/verify.sh

The instruction itself

10 sections, as written by the author

Neon — serverless Postgres as a platform

Neon is Postgres with three platform features the engine doesn't have: a serverless driver that

talks over HTTP/WebSocket (so you can query from edge runtimes), built-in connection pooling, and

copy-on-write branching that makes a fork of production data appear instantly. This skill owns

that platform layer. The Postgres *engine* underneath — schema, indexes, EXPLAIN, RLS, zero-downtime

DDL — is identical to any Postgres 16 and belongs to ../postgresdb/SKILL.md. Don't re-derive engine

craft here; connect correctly and branch correctly.

When to use / When NOT to use

When to use:

  • Code imports @neondatabase/serverless, or DATABASE_URL points at *.neon.tech / *.aws.neon.tech.
  • Connecting from Vercel (Edge/Node), Cloudflare Workers, AWS Lambda, or any serverless/edge function — choosing HTTP neon() vs WebSocket Pool.
  • Setting up branching: dev branch off production, a branch per PR in CI, neonctl usage, the Neon + Vercel preview integration.
  • Neon-specific symptoms: cold start after scale-to-zero, "too many connections" despite being serverless, pooled-vs-direct string confusion, autoscaling CU sizing/cost.
  • Picking the connection string for migrations (direct) vs app queries (pooled).

When NOT to use — route to the sibling:

  • Schema design, index choice, EXPLAIN/ANALYZE, query tuning, RLS, VACUUM, partitioning, expand-contract migrations → ../postgresdb/SKILL.md. Neon adds nothing to the engine; defer.
  • Drizzle schema/migrations → ../drizzle-orm/SKILL.md; Prisma → prisma-orm. Neon supplies only the *driver adapter* line.
  • A different managed Postgres/BaaS: Supabase (auth + storage + realtime) → supabase; PlanetScale → planetscale; Turso/libSQL → sqlite-turso.
  • Where to deploy the app talking to Neon → ../vercel/SKILL.md / cloudflare / railway.
  • Backup/PITR strategy as a discipline → backups (Neon branch-as-restore is mentioned here, not owned).

Non-negotiables

  • A WebSocket Pool/Client is created and closed inside one request handler — never at module scope in a serverless function. Module scope = a connection that outlives the invocation and leaks.
  • App queries use the pooled string (host contains -pooler). Migrations / DDL / advisory locks / session features use the direct string (no -pooler). Mixing them is the #1 Neon foot-gun.
  • The HTTP neon() driver always uses the pooled endpoint — don't hand it a direct string and expect session state.
  • HTTP neon() has no interactive transactions. Need BEGIN ... COMMIT across round-trips? Use sql.transaction([...]) (batched, non-interactive) or a WebSocket Pool.
  • On Node.js ≤ 21 you must set neonConfig.webSocketConstructor = ws before opening a Pool/Client. Node 22+ and edge runtimes have a global WebSocket; don't set it there.
  • Pin the driver: @neondatabase/serverless is at v1.1.0 (npm latest, 2026-04-17). It is a drop-in replacement for pg.

Pick your connection method

Keyed by runtime and what the query needs. Pick the lightest transport that satisfies the need.

| You need | Use | Why |

| --- | --- | --- |

| One independent read/write per request (edge, Lambda, RSC) | HTTP neon() | Single fetch round-trip (~3 round trips vs ~8 for TCP setup); no connection to manage or leak. |

| Several queries that must be atomic, no logic between them | sql.transaction([q1, q2]) over HTTP | One round-trip, real transaction, still no socket lifecycle. |

| Interactive transaction — read a row, branch in app code, then write | WebSocket Pool (open+close in handler) | HTTP can't hold BEGIN open; WebSocket keeps the session alive within the request. |

| Full pg-API compatibility (cursors, LISTEN/NOTIFY, COPY) | WebSocket Client/Pool | HTTP is one-shot; node-postgres semantics need the socket. |

| Migrations / DDL / pg_advisory_lock / session GUCs | direct (non--pooler) string with pg or WebSocket Client | PgBouncer transaction-pooling breaks session-level state. |

If you're behind Drizzle or Prisma, you don't call these directly — you pass the driver to the

adapter. See the ORM pointers below.

Connection strings: pooled vs direct

Neon gives every branch two connection strings that differ in one thing: the host of the pooled

one contains -pooler.

pooled:  postgresql://user:[email protected]/db?sslmode=require
direct:  postgresql://user:[email protected]/db?sslmode=require

| Use case | String | Reason |

| --- | --- | --- |

| App runtime (queries from your functions) | pooled (-pooler) | PgBouncer fans many short-lived serverless invocations onto a small pool; up to 10,000 concurrent client connections per project. |

| HTTP neon() driver | pooled (it forces it regardless) | The HTTP path is stateless; pooling is the right model. |

| Migrations, DDL, schema introspection | direct | Transaction pooling drops session state; advisory locks and prepared statements need a stable session. |

| psql interactive / debugging | direct | You want one real session, not a pooled handle. |

Bad → Good for a typical setup:

# Bad: one string everywhere — migrations sporadically fail under PgBouncer.
DATABASE_URL="postgresql://...-pooler.../db?sslmode=require"

# Good: split them. App uses the pooled one; the migration tool uses DIRECT.
DATABASE_URL="postgresql://...-pooler.../db?sslmode=require"          # app runtime
DIRECT_URL="postgresql://....../db?sslmode=require"                    # migrations/DDL

Serverless driver patterns

All examples assume @neondatabase/[email protected].

HTTP one-shot — the default for edge/serverless reads and writes:

import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!); // pooled endpoint, no socket to manage
export async function getUser(id: string) {
  const [row] = await sql`select id, email from users where id = ${id}`;
  return row; // parameterized; ${id} is bound, not interpolated
}

Batched atomic writes over HTTP — atomicity without a WebSocket and without interactivity:

import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);
await sql.transaction([
  sql`insert into orders (id, total) values (${id}, ${total})`,
  sql`update inventory set qty = qty - 1 where sku = ${sku}`,
]); // one round-trip, both succeed or both roll back

Interactive transaction — WebSocket Pool, opened and closed inside the handler:

import { Pool, neonConfig } from "@neondatabase/serverless";
import ws from "ws"; // Node ≤21 only; omit on edge / Node 22+

neonConfig.webSocketConstructor = ws; // required on Node ≤21; do NOT set on edge runtimes

export async function transfer(from: string, to: string, cents: number) {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // inside the handler
  const client = await pool.connect();
  try {
    await client.query("begin");
    const { rows } = await client.query("select balance from accounts where id=$1 for update", [from]);
    if (rows[0].balance < cents) throw new Error("insufficient");
    await client.query("update accounts set balance=balance-$1 where id=$2", [cents, from]);
    await client.query("update accounts set balance=balance+$1 where id=$2", [cents, to]);
    await client.query("commit");
  } catch (e) {
    await client.query("rollback");
    throw e;
  } finally {
    client.release();
    await pool.end(); // close before the function returns — no leaked connection
  }
}

Edge runtime (Vercel Edge, Cloudflare Workers): use HTTP neon(); the global WebSocket exists, so a

Pool works without ws, but prefer HTTP unless you truly need an interactive transaction.

ORM adapters — you wire the driver, the ORM owns the query API:

  • Drizzle: pass neon()/Pool into drizzle-orm/neon-http or neon-serverless../drizzle-orm/SKILL.md.
  • Prisma: use the @prisma/adapter-neon driver adapter → prisma-orm.

Branching for dev / preview / CI

A Neon branch is copy-on-write: created instantly with no data copied, it forks production at a

point in time, gets its own compute endpoint, and scales to zero independently. That makes a branch a

cheap, isolated, full-data environment — not a backup substitute (test the restore).

| Situation | Do | Why |

| --- | --- | --- |

| Per-developer or per-PR isolated data | branch off production | Instant, real prod-shaped data, separate endpoint, auto scale-to-zero → near-free idle. |

| A genuinely separate product/tenant with its own billing & limits | new project | Branches share the project's quotas and pooled-connection ceiling. |

| Throwaway query you can run against staging | same branch | Don't manufacture a branch for a one-off SELECT. |

neonctl (alias neon) drives create/connect/teardown:

neonctl branches create --name pr-$PR_NUMBER --parent main
neonctl connection-string pr-$PR_NUMBER --pooled   # feed to the preview app
neonctl branches delete pr-$PR_NUMBER              # on PR merge/close

Vercel preview integration: with the Neon + Vercel integration, each preview deployment gets its

own branch forked from production, auto-deleted when the PR merges or closes — zero CI code. If you're

not on Vercel, replicate it in CI: create branch on PR open → run migrations (direct string) → seed →

expose the pooled connection string to the app → delete on PR close. Full GitHub Actions workflow,

neonctl reference, and the Neon API endpoints: branching-ci.

Branch-as-restore (PITR): to recover, branch from an earlier point in time and either read from it

or promote it. It's a fast undo, not a tested backup discipline — own that in backups.

Autoscaling & scale-to-zero

Each branch's compute endpoint autoscales between a min and max CU (vCPU/RAM) on its own, with no

noisy neighbors across branches. Idle compute scales to zero after 5 minutes of inactivity

(default), then resumes on the next connection with a millisecond-to-sub-second cold start.

Cost mental model: non-prod branches sit at zero almost always, so a dozen PR branches cost close to

nothing. The cold start is the price.

Prod endpoint sizing checklist:

  • [ ] Set a sane min/max CU for the prod branch — not maxed "just in case" (you pay for the max headroom you actually use).
  • [ ] Latency-sensitive prod with sparse traffic? Disable scale-to-zero or set min CU > 0 so there's no cold start in p99.
  • [ ] Treat the cold start as real: warm the connection on deploy, or accept it on non-prod and account for it in latency SLOs.
  • [ ] Keep non-prod branches on scale-to-zero — that's where the near-free economics come from.

Anti-patterns

| Anti-pattern | Why it bites | Do instead |

| --- | --- | --- |

| new Pool(...) at module scope in a serverless function | Connection outlives the invocation; you exhaust the pool → "too many connections". | Create and await pool.end() inside the handler, or use HTTP neon(). |

| Pooled (-pooler) string for migrations/DDL | PgBouncer transaction-pooling drops session state; advisory locks/prepared statements fail intermittently. | Use the direct string for migrations; pooled only for app runtime. |

| Direct (non--pooler) string as the app DATABASE_URL | No pooling → serverless concurrency exhausts raw connections fast. | App runtime always uses the pooled string. |

| Expecting BEGIN/interactive txns from HTTP neon() | HTTP is one-shot, stateless — there's no open session to hold a transaction. | sql.transaction([...]) for batched atomicity, or a WebSocket Pool for interactive. |

| Opening a Pool without neonConfig.webSocketConstructor = ws on Node ≤21 | No global WebSocket → the connection silently fails. | Set webSocketConstructor = ws on Node ≤21; omit on edge / Node 22+. |

| Ignoring scale-to-zero in p99 latency | First request after 5 min idle pays the resume cold start; users see a slow request. | Disable scale-to-zero or set min CU > 0 on latency-sensitive prod. |

| One shared branch for all PRs | PRs clobber each other's data; no isolation. | One branch per PR, deleted on close. |

| Treating a branch as a backup without testing restore | Branches share project quotas and aren't an exercised recovery path. | Verify branch-as-restore; own real backup strategy in backups. |

| Maxing CU "to be safe" | You pay for headroom you don't use; autoscaling already handles spikes. | Set a realistic min/max; let autoscaling expand. |

| Doing schema/index design in this skill | Engine craft is identical to any Postgres; duplicating it drifts. | Defer to ../postgresdb/SKILL.md. |

Cross-references

  • Engine craft (schema, indexes, EXPLAIN, RLS, zero-downtime DDL): ../postgresdb/SKILL.md.
  • Next.js App Router data layer that calls Neon: ../nextjs/SKILL.md.
  • ORM query APIs over the Neon driver: ../drizzle-orm/SKILL.md, prisma-orm.
  • Deployment targets: ../vercel/SKILL.md, cloudflare, railway. Backups discipline: backups.
  • Other managed DBs (route away): supabase, planetscale, sqlite-turso.

How to use it

Copy the folder

Take ericrisco/neon from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.