git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/dist/plugins/api-baas-vercel-postgres/skills/api-baas-vercel-postgres" ~/.claude/skills/agents-inc-skills-api-baas-vercel-postgres && rm -rf "$T"
dist/plugins/api-baas-vercel-postgres/skills/api-baas-vercel-postgres/SKILL.mdVercel Postgres Patterns
Quick Guide:
is a thin wrapper around@vercel/postgresthat auto-connects from@neondatabase/serverlessenv vars. Use thePOSTGRES_URLtagged template for one-shot queries (edge-compatible, auto-pooled). Usesqlto get a client for multi-query sequences. On edge runtimes, connections cannot be reused between requests (sql.connect()). This package is deprecated (Dec 2024) -- for new projects, usemaxUses: 1directly.@neondatabase/serverless
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use the
tagged template for one-shot queries -- it auto-connects from sql
and handles pooling)POSTGRES_URL
(You MUST call
after using a client obtained from client.release()
or sql.connect()
-- leaked clients exhaust the connection pool)pool.connect()
(You MUST use
for pooled connections and POSTGRES_URL
for direct connections -- mixing them causes connection exhaustion or feature limitations)POSTGRES_URL_NON_POOLING
(You MUST acknowledge that
is deprecated -- recommend @vercel/postgres
for new projects)@neondatabase/serverless
</critical_requirements>
Auto-detection: @vercel/postgres, vercel postgres, POSTGRES_URL, POSTGRES_URL_NON_POOLING, sql tagged template vercel, createPool vercel, createClient vercel, VercelPool, VercelClient
When to use:
- Maintaining existing projects that already use
@vercel/postgres - Querying Postgres from edge/serverless functions on Vercel
- Simple database access with auto-connection from environment variables
- Migrating away from
to@vercel/postgres@neondatabase/serverless
Key patterns covered:
tagged template (auto-pooled, edge-compatible, one-shot queries)sql
for multi-query client sessionssql.connect()
/createPool()
for custom configurationscreateClient()- Environment variables (
,POSTGRES_URL
)POSTGRES_URL_NON_POOLING - Edge vs Node.js runtime differences
- Migration path to
@neondatabase/serverless
When NOT to use:
- New projects (use
directly)@neondatabase/serverless - Long-lived server processes with persistent connections (use standard
driver)pg - General PostgreSQL query syntax (use a SQL/Postgres skill)
Detailed Resources:
- For decision frameworks and quick lookup tables, see reference.md
Examples:
- examples/core.md -- sql tagged template, createPool, createClient, edge patterns, migration
<philosophy>
Philosophy
@vercel/postgres is a convenience wrapper around @neondatabase/serverless that simplifies connection management for Vercel-deployed applications. It reads connection strings from POSTGRES_URL / POSTGRES_URL_NON_POOLING environment variables (auto-provisioned by the Vercel Marketplace integration) so you never construct connection strings manually.
Core principles:
- Zero-config connections -- The
export auto-connects from environment variables. No connection string setup needed in code.sql - Tagged template safety --
is a tagged template literal, not a function. Parameters are auto-parameterized, preventing SQL injection.sql - Pooling by default --
andsql
use the pooled connection string (createPool()
).POSTGRES_URL
uses the direct string (createClient()
).POSTGRES_URL_NON_POOLING - Edge-aware -- On edge runtimes, the SDK sets
because IO connections cannot survive between requests. For multi-query in a single request, usemaxUses: 1
.sql.connect()
Deprecation context:
Vercel Postgres was sunset in December 2024. All databases were migrated to Neon. The
@vercel/postgres npm package (v0.10.0) is no longer maintained. Two migration paths exist:
- Drop-in replacement:
(same API, maintained by Neon)@neondatabase/vercel-postgres-compat - Full migration:
(actively developed, richer API with HTTP transactions and composable fragments)@neondatabase/serverless
<patterns>
Core Patterns
Pattern 1: One-Shot Queries with sql
sqlThe
sql export is a tagged template that auto-connects from POSTGRES_URL. It handles pooling internally.
import { sql } from "@vercel/postgres"; // Tagged template -- values are auto-parameterized (safe from injection) const ACTIVE_STATUS = "active"; const { rows } = await sql`SELECT id, name FROM users WHERE status = ${ACTIVE_STATUS}`;
Why good: Zero-config (reads POSTGRES_URL automatically), auto-parameterized preventing SQL injection, connection pooling handled internally, named constant for status
// BAD: String interpolation instead of tagged template const status = "active"; const { rows } = await sql.query( `SELECT * FROM users WHERE status = '${status}'`, );
Why bad: String interpolation bypasses parameterization -- SQL injection vulnerability, uses
.query() with string instead of tagged template
Pattern 2: Multi-Query Sessions with sql.connect()
sql.connect()When you need multiple queries on the same connection (transactions, sequential operations), obtain a client.
import { sql } from "@vercel/postgres"; async function transferFunds(fromId: string, toId: string, amount: number) { const client = await sql.connect(); try { await client.sql`BEGIN`; await client.sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`; await client.sql`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`; await client.sql`COMMIT`; } catch (error) { await client.sql`ROLLBACK`; throw error; } finally { client.release(); } }
Why good: Client obtained from pool for multi-query, explicit BEGIN/COMMIT/ROLLBACK for transaction,
client.release() in finally block prevents connection leaks, uses tagged template on client
// BAD: Multiple sql calls without a shared client import { sql } from "@vercel/postgres"; await sql`BEGIN`; // Gets connection A await sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`; // Gets connection B! await sql`COMMIT`; // Gets connection C -- BEGIN was on A, this COMMIT does nothing useful
Why bad: Each
sql call may use a different connection from the pool -- BEGIN/COMMIT on different connections means the transaction is not atomic
Pattern 3: Custom Pool with createPool()
createPool()Use
createPool() when you need custom configuration or a non-default connection string.
import { createPool } from "@vercel/postgres"; // Custom pool with explicit connection string const pool = createPool({ connectionString: process.env.SECONDARY_POSTGRES_URL, }); const { rows } = await pool.sql`SELECT id, title FROM posts WHERE published = true`;
Why good: Useful for connecting to a secondary database, pool provides same
sql tagged template interface, explicit connection string when POSTGRES_URL isn't appropriate
Custom Client with createClient()
createClient()import { createClient } from "@vercel/postgres"; // Direct (non-pooled) connection -- reads POSTGRES_URL_NON_POOLING by default const client = createClient(); await client.connect(); try { const { rows } = await client.sql`SELECT id, name FROM users LIMIT 10`; return rows; } finally { await client.end(); }
When to use: Migrations, administrative operations, or scenarios requiring session-level features (SET, LISTEN/NOTIFY) that PgBouncer's transaction mode does not support.
Pattern 4: Edge Runtime Considerations
On edge runtimes, IO connections cannot be reused between requests. The SDK automatically sets
maxUses: 1.
// Edge function -- single query is fine with sql import { sql } from "@vercel/postgres"; export const runtime = "edge"; export async function GET() { const { rows } = await sql`SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 10`; return Response.json(rows); }
Why good: Single
sql call works naturally on edge, no connection management needed
// Edge function -- multiple queries need a shared client import { sql } from "@vercel/postgres"; export const runtime = "edge"; export async function GET() { // sql.connect() gets a client from the pool -- reuse it for multiple queries const client = await sql.connect(); try { const { rows: posts } = await client.sql`SELECT id, title FROM posts LIMIT 10`; const { rows: counts } = await client.sql`SELECT count(*)::int AS total FROM posts`; return Response.json({ posts, total: counts[0].total }); } finally { client.release(); } }
Why good: On edge with
maxUses: 1, each pool.connect() opens a new connection -- reusing the client avoids opening multiple connections per request
Pattern 5: Migration to @neondatabase/serverless
@neondatabase/serverlessSince
@vercel/postgres is deprecated, here are the migration paths.
Drop-In Replacement (Minimal Changes)
// Before import { sql } from "@vercel/postgres"; // After -- same API, maintained by Neon import { sql } from "@neondatabase/vercel-postgres-compat"; // Code stays the same const { rows } = await sql`SELECT id, name FROM users`;
When to use: Existing projects that need a quick fix without rewriting query code.
Full Migration (Recommended for New Code)
// Before (@vercel/postgres) import { sql } from "@vercel/postgres"; const { rows } = await sql`SELECT id, name FROM users WHERE status = ${status}`; // After (@neondatabase/serverless) import { neon } from "@neondatabase/serverless"; const sql = neon(process.env.DATABASE_URL!); const rows = await sql`SELECT id, name FROM users WHERE status = ${status}`;
Key differences:
returns@vercel/postgres
--{ rows, rowCount, ... }
returns rows directly (unless@neondatabase/serverless
)fullResults: true
reads@vercel/postgres
--POSTGRES_URL
requires explicit connection string (typically@neondatabase/serverless
)DATABASE_URL
adds HTTP transactions via@neondatabase/serverless
and composable fragmentssql.transaction()
<decision_framework>
Decision Framework
Which API to Use
What kind of operation? +-- Single query (SELECT, INSERT, UPDATE, DELETE) | +-- Use sql tagged template directly +-- Multiple queries that must be atomic (transaction)? | +-- Use sql.connect() to get a client, wrap in BEGIN/COMMIT +-- Need custom connection string (not POSTGRES_URL)? | +-- Use createPool() with explicit connectionString +-- Need session-level features (SET, LISTEN/NOTIFY)? | +-- Use createClient() (reads POSTGRES_URL_NON_POOLING) +-- Starting a new project? +-- Use @neondatabase/serverless instead
Environment Variable Selection
What is the workload? +-- Serverless/edge function --> POSTGRES_URL (pooled) +-- Application queries --> POSTGRES_URL (pooled) +-- Schema migrations --> POSTGRES_URL_NON_POOLING (direct) +-- LISTEN/NOTIFY --> POSTGRES_URL_NON_POOLING (direct) +-- pg_dump / pg_restore --> POSTGRES_URL_NON_POOLING (direct)
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Using
for transactions withoutsql
-- Eachsql.connect()
tagged template call may use a different pooled connection. BEGIN on one connection and COMMIT on another means no transaction at all.sql - Forgetting
afterclient.release()
-- Leaked clients exhaust the connection pool, causing all subsequent queries to hang until timeout.sql.connect() - Using
for migrations -- The pooled connection runs through PgBouncer in transaction mode, which breaks session-level features needed by migration tools.POSTGRES_URL
Medium Priority Issues:
- String interpolation instead of tagged template --
is safe.sql`...${value}...`
...${value}...`)` is SQL injection.sql.query(\ - Creating pools/clients without closing them --
requires explicitcreateClient()
. Forgetting it leaks connections.client.end() - Ignoring deprecation --
v0.10.0 is the last version. No security patches or bug fixes will be released.@vercel/postgres
Gotchas & Edge Cases:
- Edge runtime
-- On edge, the pool cannot reuse connections within a request. If you fire multiplemaxUses: 1
calls, each opens a new connection. Usesql
to share one.sql.connect()
is a tagged template, not a function --sql
is wrong.sql(...)
is correct. This is a common error when copying from non-Vercel Postgres examples.sql`...`
vsPOSTGRES_URL
--DATABASE_URL
reads@vercel/postgres
by default.POSTGRES_URL
reads nothing by default (pass explicitly). After Neon migration, Vercel sets both, but your code must match the SDK's expectation.@neondatabase/serverless- PgBouncer transaction mode limitations -- Through pooled connections: no SET/RESET, no LISTEN/NOTIFY, no temporary tables with PRESERVE, no session-level advisory locks.
- Result shape differs from
--@neondatabase/serverless
returns@vercel/postgres
. The Neon{ rows, rowCount, fields }
function returns rows directly. This breaks code during migration if not accounted for.neon()
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use the
tagged template for one-shot queries -- it auto-connects from sql
and handles pooling)POSTGRES_URL
(You MUST call
after using a client obtained from client.release()
or sql.connect()
-- leaked clients exhaust the connection pool)pool.connect()
(You MUST use
for pooled connections and POSTGRES_URL
for direct connections -- mixing them causes connection exhaustion or feature limitations)POSTGRES_URL_NON_POOLING
(You MUST acknowledge that
is deprecated -- recommend @vercel/postgres
for new projects)@neondatabase/serverless
Failure to follow these rules will cause connection pool exhaustion, SQL injection vulnerabilities, or silent transaction failures.
</critical_reminders>