dist/plugins/api-baas-vercel-postgres/skills/api-baas-vercel-postgres/SKILL.md
Serverless PostgreSQL on Vercel with edge-compatible SDK
npx skillsauth add agents-inc/skills api-baas-vercel-postgresInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide:
@vercel/postgresis a thin wrapper around@neondatabase/serverlessthat auto-connects fromPOSTGRES_URLenv vars. Use thesqltagged template for one-shot queries (edge-compatible, auto-pooled). Usesql.connect()to get a client for multi-query sequences. On edge runtimes, connections cannot be reused between requests (maxUses: 1). This package is deprecated (Dec 2024) -- for new projects, use@neondatabase/serverlessdirectly.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use the sql tagged template for one-shot queries -- it auto-connects from POSTGRES_URL and handles pooling)
(You MUST call client.release() after using a client obtained from sql.connect() or pool.connect() -- leaked clients exhaust the connection pool)
(You MUST use POSTGRES_URL for pooled connections and POSTGRES_URL_NON_POOLING for direct connections -- mixing them causes connection exhaustion or feature limitations)
(You MUST acknowledge that @vercel/postgres is deprecated -- recommend @neondatabase/serverless for new projects)
</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:
@vercel/postgres@vercel/postgres to @neondatabase/serverlessKey patterns covered:
sql tagged template (auto-pooled, edge-compatible, one-shot queries)sql.connect() for multi-query client sessionscreatePool() / createClient() for custom configurationsPOSTGRES_URL, POSTGRES_URL_NON_POOLING)@neondatabase/serverlessWhen NOT to use:
@neondatabase/serverless directly)pg driver)Detailed Resources:
Examples:
@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:
sql export auto-connects from environment variables. No connection string setup needed in code.sql is a tagged template literal, not a function. Parameters are auto-parameterized, preventing SQL injection.sql and createPool() use the pooled connection string (POSTGRES_URL). createClient() uses the direct string (POSTGRES_URL_NON_POOLING).maxUses: 1 because IO connections cannot survive between requests. For multi-query in a single request, use 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:
@neondatabase/vercel-postgres-compat (same API, maintained by Neon)@neondatabase/serverless (actively developed, richer API with HTTP transactions and composable fragments)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
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
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
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.
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
@neondatabase/serverlessSince @vercel/postgres is deprecated, here are the migration paths.
// 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.
// 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:
@vercel/postgres returns { rows, rowCount, ... } -- @neondatabase/serverless returns rows directly (unless fullResults: true)@vercel/postgres reads POSTGRES_URL -- @neondatabase/serverless requires explicit connection string (typically DATABASE_URL)@neondatabase/serverless adds HTTP transactions via sql.transaction() and composable fragments<decision_framework>
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
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>
High Priority Issues:
sql for transactions without sql.connect() -- Each sql tagged template call may use a different pooled connection. BEGIN on one connection and COMMIT on another means no transaction at all.client.release() after sql.connect() -- Leaked clients exhaust the connection pool, causing all subsequent queries to hang until timeout.POSTGRES_URL for migrations -- The pooled connection runs through PgBouncer in transaction mode, which breaks session-level features needed by migration tools.Medium Priority Issues:
sql`...${value}...` is safe. sql.query(\...${value}...`)` is SQL injection.createClient() requires explicit client.end(). Forgetting it leaks connections.@vercel/postgres v0.10.0 is the last version. No security patches or bug fixes will be released.Gotchas & Edge Cases:
maxUses: 1 -- On edge, the pool cannot reuse connections within a request. If you fire multiple sql calls, each opens a new connection. Use sql.connect() to share one.sql 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.POSTGRES_URL vs DATABASE_URL -- @vercel/postgres reads POSTGRES_URL by default. @neondatabase/serverless reads nothing by default (pass explicitly). After Neon migration, Vercel sets both, but your code must match the SDK's expectation.@neondatabase/serverless -- @vercel/postgres returns { rows, rowCount, fields }. The Neon neon() function returns rows directly. This breaks code during migration if not accounted for.</red_flags>
<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 sql tagged template for one-shot queries -- it auto-connects from POSTGRES_URL and handles pooling)
(You MUST call client.release() after using a client obtained from sql.connect() or pool.connect() -- leaked clients exhaust the connection pool)
(You MUST use POSTGRES_URL for pooled connections and POSTGRES_URL_NON_POOLING for direct connections -- mixing them causes connection exhaustion or feature limitations)
(You MUST acknowledge that @vercel/postgres is deprecated -- recommend @neondatabase/serverless for new projects)
Failure to follow these rules will cause connection pool exhaustion, SQL injection vulnerabilities, or silent transaction failures.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety