Claude Skill

api-database-vercel-postgres

Serverless PostgreSQL on Vercel with edge-compatible SDK

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-database-vercel-postgres_skills_api-database-vercel-postgres-3a51ef5.zip · 8 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-vercel-postgres/skills/api-database-vercel-postgres
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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



<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(\...$...`)` 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>

Files (skills)
  • examples
    • core.md 8.1 KB
      # Vercel Postgres -- Core Examples
      
      > Driver setup, queries, transactions, edge patterns, and migration. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Basic Query with `sql`
      
      ### Good Example -- Tagged Template Query
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      const ACTIVE_STATUS = "active";
      const PAGE_SIZE = 20;
      
      async function getActiveUsers(page: number) {
        const offset = page * PAGE_SIZE;
      
        const { rows } = await sql`
          SELECT id, name, email
          FROM users
          WHERE status = ${ACTIVE_STATUS}
          ORDER BY created_at DESC
          LIMIT ${PAGE_SIZE} OFFSET ${offset}
        `;
      
        return rows;
      }
      ```
      
      **Why good:** Tagged template auto-parameterizes `${ACTIVE_STATUS}`, `${PAGE_SIZE}`, and `${offset}` preventing SQL injection, named constants for magic values, auto-connects from `POSTGRES_URL`
      
      ### Bad Example -- String Interpolation
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      async function getUsers(status: string) {
        // BAD: sql.query with string template -- SQL injection
        const { rows } = await sql.query(
          `SELECT * FROM users WHERE status = '${status}'`,
        );
        return rows;
      }
      ```
      
      **Why bad:** String interpolation bypasses parameterization, user-supplied `status` can inject arbitrary SQL
      
      ---
      
      ## Pattern 2: Insert with Returning
      
      ### Good Example -- Typed Insert
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      interface User {
        id: string;
        name: string;
        email: string;
      }
      
      async function createUser(name: string, email: string): Promise<User> {
        const { rows } = await sql<User>`
          INSERT INTO users (name, email)
          VALUES (${name}, ${email})
          RETURNING id, name, email
        `;
      
        return rows[0];
      }
      ```
      
      **Why good:** Generic type parameter `<User>` types the result rows, RETURNING avoids a separate SELECT, single tagged template call
      
      ---
      
      ## Pattern 3: Transaction with `sql.connect()`
      
      ### Good Example -- Proper Transaction Pattern
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      async function createOrderWithItems(
        userId: string,
        items: Array<{ productId: string; quantity: number; price: number }>,
      ) {
        const client = await sql.connect();
      
        try {
          await client.sql`BEGIN`;
      
          // Create order
          const {
            rows: [order],
          } = await client.sql`
            INSERT INTO orders (user_id, status)
            VALUES (${userId}, 'pending')
            RETURNING id
          `;
      
          // Insert all items
          for (const item of items) {
            await client.sql`
              INSERT INTO order_items (order_id, product_id, quantity, unit_price)
              VALUES (${order.id}, ${item.productId}, ${item.quantity}, ${item.price})
            `;
          }
      
          await client.sql`COMMIT`;
          return { orderId: order.id };
        } catch (error) {
          await client.sql`ROLLBACK`;
          throw error;
        } finally {
          client.release();
        }
      }
      ```
      
      **Why good:** `sql.connect()` gets a dedicated client from the pool, all queries run on same connection (transaction is real), ROLLBACK on error, `client.release()` in finally prevents leaks
      
      ### Bad Example -- Transaction Without Shared Client
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      // BAD: Each sql call may hit a different pooled connection
      async function badTransaction(userId: string, amount: number) {
        await sql`BEGIN`;
        await sql`UPDATE accounts SET balance = balance - ${amount} WHERE user_id = ${userId}`;
        await sql`COMMIT`;
        // BEGIN was on connection A, UPDATE on B, COMMIT on C -- no real transaction!
      }
      ```
      
      **Why bad:** The `sql` export uses a pool -- each call may get a different connection, making BEGIN/COMMIT meaningless across connections
      
      ---
      
      ## Pattern 4: Custom Pool Configuration
      
      ### Good Example -- Secondary Database
      
      ```typescript
      import { createPool } from "@vercel/postgres";
      
      // Connect to a different database than the default POSTGRES_URL
      const analyticsPool = createPool({
        connectionString: process.env.ANALYTICS_POSTGRES_URL,
      });
      
      async function getPageViews(path: string) {
        const { rows } = await analyticsPool.sql`
          SELECT date, views
          FROM page_analytics
          WHERE path = ${path}
          ORDER BY date DESC
          LIMIT 30
        `;
      
        return rows;
      }
      ```
      
      **Why good:** `createPool()` with explicit connection string for secondary databases, pool provides same `sql` tagged template interface
      
      ---
      
      ## Pattern 5: Edge Runtime Multi-Query
      
      ### Good Example -- Shared Client on Edge
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      export const runtime = "edge";
      
      export async function GET(request: Request) {
        // On edge, maxUses=1 means each pool.connect() opens a fresh connection.
        // Use one client for all queries to avoid opening N connections.
        const client = await sql.connect();
      
        try {
          const { rows: posts } = await client.sql`
            SELECT id, title, excerpt FROM posts WHERE published = true ORDER BY created_at DESC LIMIT 10
          `;
      
          const {
            rows: [{ count }],
          } = await client.sql`
            SELECT count(*)::int FROM posts WHERE published = true
          `;
      
          return Response.json({ posts, total: count });
        } finally {
          client.release();
        }
      }
      ```
      
      **Why good:** Single client for multiple queries on edge (avoids opening multiple connections), `client.release()` in finally
      
      ### Bad Example -- Multiple `sql` Calls on Edge
      
      ```typescript
      import { sql } from "@vercel/postgres";
      
      export const runtime = "edge";
      
      export async function GET() {
        // BAD on edge: each sql call opens a NEW connection (maxUses=1)
        const { rows: posts } = await sql`SELECT * FROM posts LIMIT 10`;
        const { rows: users } = await sql`SELECT * FROM users LIMIT 10`;
        const { rows: tags } = await sql`SELECT * FROM tags`;
        // 3 separate connections opened and closed -- wasteful
        return Response.json({ posts, users, tags });
      }
      ```
      
      **Why bad:** On edge runtime, `maxUses: 1` means each `sql` call opens a new TCP/WebSocket connection, tripling connection overhead and latency
      
      ---
      
      ## Pattern 6: Direct Client for Migrations
      
      ### Good Example -- Non-Pooled Connection
      
      ```typescript
      import { createClient } from "@vercel/postgres";
      
      // createClient reads POSTGRES_URL_NON_POOLING by default (direct, no PgBouncer)
      async function runMigration() {
        const client = createClient();
        await client.connect();
      
        try {
          await client.sql`
            CREATE TABLE IF NOT EXISTS users (
              id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
              name TEXT NOT NULL,
              email TEXT UNIQUE NOT NULL,
              status TEXT NOT NULL DEFAULT 'active',
              created_at TIMESTAMPTZ NOT NULL DEFAULT now()
            )
          `;
        } finally {
          await client.end();
        }
      }
      ```
      
      **Why good:** `createClient()` uses direct connection (`POSTGRES_URL_NON_POOLING`) which supports DDL and session features, explicit `client.end()` cleanup
      
      **When to use:** Schema migrations, `pg_dump`, or any operation needing session-level features that PgBouncer's transaction mode strips.
      
      ---
      
      ## Pattern 7: Migration to `@neondatabase/serverless`
      
      ### Good Example -- Full Migration
      
      ```typescript
      // 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:**
      
      - `neon()` requires an explicit connection string (typically `DATABASE_URL`) -- no auto-read from env
      - `neon()` returns rows directly by default -- not `{ rows, rowCount, ... }`. Use `{ fullResults: true }` option to get the full result object
      - `@neondatabase/serverless` supports HTTP transactions via `sql.transaction([...])` and composable SQL fragments
      
      ### WebSocket Pool Migration (Transactions)
      
      ```typescript
      // Before (@vercel/postgres)
      import { sql } from "@vercel/postgres";
      const client = await sql.connect();
      
      // After (@neondatabase/serverless)
      import { Pool } from "@neondatabase/serverless";
      const pool = new Pool({ connectionString: process.env.DATABASE_URL });
      const client = await pool.connect();
      ```
      
      **Why good:** `Pool` from `@neondatabase/serverless` provides the same `connect()` / `release()` pattern. Transactions work identically with `BEGIN`/`COMMIT`/`ROLLBACK` on the client.
      
      ---
      
      _For decision frameworks and API reference, see [reference.md](../reference.md)._
      
  • reference.md 4.6 KB
    # Vercel Postgres Reference
    
    > Quick lookup tables and environment variable reference. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples.
    
    ---
    
    ## Deprecation Status
    
    | Detail                       | Value                                    |
    | ---------------------------- | ---------------------------------------- |
    | Last version                 | 0.10.0                                   |
    | Status                       | Deprecated (December 2024)               |
    | Databases migrated to        | Neon (automatic, via Vercel Marketplace) |
    | Recommended for new projects | `@neondatabase/serverless`               |
    
    ---
    
    ## API Exports
    
    | Export                     | Import                                                        | Description                                            |
    | -------------------------- | ------------------------------------------------------------- | ------------------------------------------------------ |
    | `sql`                      | `import { sql } from "@vercel/postgres"`                      | Auto-connected tagged template (pooled)                |
    | `createPool`               | `import { createPool } from "@vercel/postgres"`               | Custom connection pool                                 |
    | `createClient`             | `import { createClient } from "@vercel/postgres"`             | Single direct connection                               |
    | `db`                       | `import { db } from "@vercel/postgres"`                       | Alias for `sql` (pool-based access)                    |
    | `postgresConnectionString` | `import { postgresConnectionString } from "@vercel/postgres"` | Returns connection URL from env vars (`pool`/`direct`) |
    
    ---
    
    ## Environment Variables
    
    ```bash
    # Pooled connection (via PgBouncer -- for application queries)
    POSTGRES_URL=postgresql://user:pass@endpoint-pooler.region.aws.neon.tech/dbname?sslmode=require
    
    # Direct connection (for migrations, session features)
    POSTGRES_URL_NON_POOLING=postgresql://user:pass@endpoint.region.aws.neon.tech/dbname?sslmode=require
    ```
    
    These are auto-provisioned by the Vercel Marketplace integration. Pull locally with `vercel env pull .env.development.local`.
    
    ---
    
    ## `sql` Methods
    
    | Method                    | Description                                                 |
    | ------------------------- | ----------------------------------------------------------- |
    | `` sql`...` ``            | Execute a single parameterized query (tagged template)      |
    | `sql.connect()`           | Get a `VercelPoolClient` for multi-query sessions           |
    | `sql.query(text, values)` | Execute a query with explicit text + params (pg-compatible) |
    
    ---
    
    ## Edge vs Node.js Runtime
    
    | Behavior                         | Node.js                         | Edge                        |
    | -------------------------------- | ------------------------------- | --------------------------- |
    | Connection reuse across requests | Yes                             | No                          |
    | `maxUses` setting                | Default (unlimited)             | `1` (auto-set by SDK)       |
    | Multiple `sql` calls per request | Each may reuse connections      | Each opens a new connection |
    | Recommended for multi-query      | `sql.connect()` or direct `sql` | `sql.connect()` (required)  |
    
    ---
    
    ## PgBouncer Transaction Mode Limitations
    
    Through pooled connections (`POSTGRES_URL`), the following are **not supported**:
    
    - `SET` / `RESET` statements
    - `LISTEN` / `NOTIFY`
    - `WITH HOLD CURSOR`
    - Session-level advisory locks
    - Temporary tables with `PRESERVE` / `DELETE ROWS`
    - SQL-level `PREPARE` / `DEALLOCATE`
    
    Use `POSTGRES_URL_NON_POOLING` (direct connection) for these features.
    
    ---
    
    ## Migration Cheat Sheet
    
    | `@vercel/postgres`                       | `@neondatabase/serverless`                                 |
    | ---------------------------------------- | ---------------------------------------------------------- |
    | `import { sql } from "@vercel/postgres"` | `import { neon } from "@neondatabase/serverless"`          |
    | `sql` auto-reads `POSTGRES_URL`          | `const sql = neon(process.env.DATABASE_URL!)`              |
    | `` const { rows } = await sql`...` ``    | `` const rows = await sql`...` ``                          |
    | `sql.connect()` for client               | `Pool` + `pool.connect()`                                  |
    | `createPool()`                           | `new Pool({ connectionString })`                           |
    | `createClient()`                         | `new Client({ connectionString })`                         |
    | Transaction: `BEGIN`/`COMMIT` via client | `sql.transaction([...])` (HTTP) or client `BEGIN`/`COMMIT` |
    
  • SKILL.md 10.5 KB
    ---
    name: api-database-vercel-postgres
    description: Serverless PostgreSQL on Vercel with edge-compatible SDK
    ---
    
    # 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](reference.md)
    
    **Examples:**
    
    - [examples/core.md](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. Migration path:
    
    - **Full migration (recommended):** `@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`. Values are auto-parameterized (preventing SQL injection). See [examples/core.md](examples/core.md) for full examples with good/bad comparisons.
    
    ```typescript
    import { sql } from "@vercel/postgres";
    
    const ACTIVE_STATUS = "active";
    const { rows } =
      await sql`SELECT id, name FROM users WHERE status = ${ACTIVE_STATUS}`;
    ```
    
    ---
    
    ### Pattern 2: Multi-Query Sessions with `sql.connect()`
    
    When you need multiple queries on the same connection (transactions, sequential operations), obtain a client. Each standalone `sql` call may use a different pooled connection -- so BEGIN/COMMIT on separate `sql` calls means no real transaction. See [examples/core.md](examples/core.md) for transaction patterns.
    
    ```typescript
    const client = await sql.connect();
    try {
      await client.sql`BEGIN`;
      // ... queries on same client ...
      await client.sql`COMMIT`;
    } catch (error) {
      await client.sql`ROLLBACK`;
      throw error;
    } finally {
      client.release();
    }
    ```
    
    ---
    
    ### Pattern 3: Custom Pool and Client
    
    `createPool()` for custom connection strings (secondary databases). `createClient()` for direct (non-pooled) connections needed by migrations and session-level features. See [examples/core.md](examples/core.md) for full examples.
    
    ```typescript
    import { createPool } from "@vercel/postgres";
    const pool = createPool({
      connectionString: process.env.SECONDARY_POSTGRES_URL,
    });
    const { rows } =
      await pool.sql`SELECT id, title FROM posts WHERE published = true`;
    ```
    
    ---
    
    ### Pattern 4: Edge Runtime Considerations
    
    On edge runtimes, the SDK sets `maxUses: 1` -- connections cannot be reused between requests. Single `sql` calls work fine, but for multiple queries use `sql.connect()` to share one connection. See [examples/core.md](examples/core.md) for edge-specific patterns.
    
    ---
    
    ### Pattern 5: Migration to `@neondatabase/serverless`
    
    Since `@vercel/postgres` is deprecated, migrate to `@neondatabase/serverless`. See [examples/core.md](examples/core.md) for full migration examples.
    
    **Key differences to be aware of:**
    
    - `@vercel/postgres` returns `{ rows, rowCount, ... }` -- `@neondatabase/serverless` `neon()` 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>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related