Skills api-baas-vercel-postgres

Serverless PostgreSQL on Vercel with edge-compatible SDK

install
source · Clone the upstream repo
git clone https://github.com/agents-inc/skills
Claude Code · Install into ~/.claude/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"
manifest: dist/plugins/api-baas-vercel-postgres/skills/api-baas-vercel-postgres/SKILL.md
source content

Vercel Postgres Patterns

Quick Guide:

@vercel/postgres
is a thin wrapper around
@neondatabase/serverless
that auto-connects from
POSTGRES_URL
env vars. Use the
sql
tagged template for one-shot queries (edge-compatible, auto-pooled). Use
sql.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/serverless
directly.


<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

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:

  • 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
    @vercel/postgres
    to
    @neondatabase/serverless

Key patterns covered:

  • sql
    tagged template (auto-pooled, edge-compatible, one-shot queries)
  • sql.connect()
    for multi-query client sessions
  • createPool()
    /
    createClient()
    for custom configurations
  • 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
    @neondatabase/serverless
    directly)
  • Long-lived server processes with persistent connections (use standard
    pg
    driver)
  • 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:

  1. Zero-config connections -- The
    sql
    export auto-connects from environment variables. No connection string setup needed in code.
  2. Tagged template safety --
    sql
    is a tagged template literal, not a function. Parameters are auto-parameterized, preventing SQL injection.
  3. Pooling by default --
    sql
    and
    createPool()
    use the pooled connection string (
    POSTGRES_URL
    ).
    createClient()
    uses the direct string (
    POSTGRES_URL_NON_POOLING
    ).
  4. Edge-aware -- On edge runtimes, the SDK sets
    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:

  • Drop-in replacement:
    @neondatabase/vercel-postgres-compat
    (same API, maintained by Neon)
  • Full migration:
    @neondatabase/serverless
    (actively developed, richer API with HTTP transactions and composable fragments)
</philosophy>
<patterns>

Core Patterns

Pattern 1: One-Shot Queries with
sql

The

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()

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()

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()

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

Since

@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:

  • @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
</patterns>

<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
    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.
  • Forgetting
    client.release()
    after
    sql.connect()
    -- Leaked clients exhaust the connection pool, causing all subsequent queries to hang until timeout.
  • Using
    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:

  • String interpolation instead of tagged template --
    sql`...${value}...`
    is safe.
    sql.query(\
    ...${value}...`)` is SQL injection.
  • Creating pools/clients without closing them --
    createClient()
    requires explicit
    client.end()
    . Forgetting it leaks connections.
  • Ignoring deprecation --
    @vercel/postgres
    v0.10.0 is the last version. No security patches or bug fixes will be released.

Gotchas & Edge Cases:

  • Edge runtime
    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.
  • 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
    --
    @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>

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>