Neon & Vercel Serverless Postgres

SkillDatabases & data

Set up serverless Postgres with Neon or Vercel Postgres for Cloudflare Workers/Edge. Includes connection pooling, git-like branching, and Drizzle ORM integration.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Neon & Vercel Serverless Postgres skill

What this skill tells your AI

The instructions your AI receives, as published by dennislee928/ethic-latex in .claude/skills/neon-vercel-postgres/SKILL.md and read by ahel’s review.

Status: Production Ready Last Updated: 2026-01-21 Dependencies: None Latest Versions: @neondatabase/serverless@1.0.2, @vercel/postgres@0.10.0, drizzle-orm@0.45.1, drizzle-kit@0.31.8, neonctl@2.19.0


Quick Start (5 Minutes)

1. Choose Your Platform

Option A: Neon Direct (multi-cloud, Cloudflare Workers, any serverless)

npm install @neondatabase/serverless

Option B: Vercel Postgres (Vercel-only, zero-config on Vercel)

npm install @vercel/postgres

Note: Both use the same Neon backend. Vercel Postgres is Neon with Vercel-specific environment setup.

Why this matters:

  • Neon direct gives you multi-cloud flexibility and access to branching API
  • Vercel Postgres gives you zero-config on Vercel with automatic environment variables
  • Both are HTTP-based (no TCP), perfect for serverless/edge environments

2. Get Your Connection String

For Neon Direct:

# Sign up at https://neon.tech
# Create a project → Get connection string
# Format: postgresql://user:password@ep-xyz.region.aws.neon.tech/dbname?sslmode=require

For Vercel Postgres:

# In your Vercel project
vercel postgres create
vercel env pull .env.local  # Automatically creates POSTGRES_URL and other vars

CRITICAL:

  • Use pooled connection string for serverless (ends with -pooler.region.aws.neon.tech)
  • Non-pooled connections will exhaust quickly in serverless environments
  • Always include ?sslmode=require parameter

3. Query Your Database

Neon Direct (Cloudflare Workers, Vercel Edge, Node.js):

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

const sql = neon(process.env.DATABASE_URL!);

// Simple query
const users = await sql`SELECT * FROM users WHERE id = ${userId}`;

// Transactions
const result = await sql.transaction([
  sql`INSERT INTO users (name) VALUES (${name})`,
  sql`SELECT * FROM users WHERE name = ${name}`
]);

Vercel Postgres (Next.js Server Actions, API Routes):

import { sql } from '@vercel/postgres';

// Simple query
const { rows } = await sql`SELECT * FROM users WHERE id = ${userId}`;

// Transactions
const client = await sql.connect();
try {
  await client.sql`BEGIN`;
  await client.sql`INSERT INTO users (name) VALUES (${name})`;
  await client.sql`COMMIT`;
} finally {
  client.release();
}

CRITICAL:

  • Use template tag syntax (sql`...`) for automatic SQL injection protection
  • Never concatenate strings: sql('SELECT * FROM users WHERE id = ' + id)
  • Template tags automatically escape values and prevent SQL injection

The 7-Step Setup Process

Step 1: Install Package

Choose based on your deployment platform:

Neon Direct (Cloudflare Workers, multi-cloud, direct Neon access):

npm install @neondatabase/serverless

Vercel Postgres (Vercel-specific, zero-config):

npm install @vercel/postgres

With ORM:

# Drizzle ORM (recommended for edge compatibility)
npm install drizzle-orm@0.45.1 @neondatabase/serverless@1.0.2
npm install -D drizzle-kit@0.31.8

# Prisma (Node.js only)
npm install prisma @prisma/client @prisma/adapter-neon @neondatabase/serverless

Key Points:

  • Both packages use HTTP/WebSocket (no TCP required)
  • Edge-compatible (works in Cloudflare Workers, Vercel Edge Runtime)
  • Connection pooling is built-in when using pooled connection strings
  • No need for separate connection pool libraries

Step 2: Create Neon Database

Option A: Neon Dashboard

  1. Sign up at https://neon.tech
  2. Create a new project
  3. Copy the pooled connection string (important!)
  4. Format: postgresql://user:pass@ep-xyz-pooler.region.aws.neon.tech/db?sslmode=require

Option B: Vercel Dashboard

  1. Go to your Vercel project → Storage → Create Database → Postgres
  2. Vercel automatically creates a Neon database
  3. Run vercel env pull to get environment variables locally

Option C: Neon CLI (neonctl@2.19.0)

# Install CLI
npm install -g neonctl@2.19.0

# Authenticate
neonctl auth

# Create project and get connection string
neonctl projects create --name my-app
neonctl connection-string main

CRITICAL:

  • Always use the pooled connection string (ends with -pooler.region.aws.neon.tech)
  • Non-pooled connections are for direct connections (not serverless)
  • Include ?sslmode=require in connection string

Step 3: Configure Environment Variables

For Neon Direct:

# .env or .env.local
DATABASE_URL="postgresql://user:password@ep-xyz-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"

For Vercel Postgres:

# Automatically created by `vercel env pull`
POSTGRES_URL="..."               # Pooled connection (use this for queries)
POSTGRES_PRISMA_URL="..."        # For Prisma migrations
POSTGRES_URL_NON_POOLING="..."   # Direct connection (avoid in serverless)
POSTGRES_USER="..."
POSTGRES_HOST="..."
POSTGRES_PASSWORD="..."
POSTGRES_DATABASE="..."

For Cloudflare Workers (wrangler.jsonc):

{
  "vars": {
    "DATABASE_URL": "postgresql://user:password@ep-xyz-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"
  }
}

Key Points:

  • Use POSTGRES_URL (pooled) for queries
  • Use POSTGRES_PRISMA_URL for Prisma migrations
  • Never use POSTGRES_URL_NON_POOLING in serverless functions
  • Store secrets securely (Vercel env, Cloudflare secrets, etc.)

Step 4: Create Database Schema

Option A: Raw SQL

// scripts/migrate.ts
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);

await sql`
  CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
  )
`;

Option B: Drizzle ORM (recommended)

// db/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow()
});
// db/index.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
# Run migrations
npx drizzle-kit generate
npx drizzle-kit migrate

Option C: Prisma

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("POSTGRES_PRISMA_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  createdAt DateTime @default(now()) @map("created_at")

  @@map("users")
}
npx prisma migrate dev --name init

CRITICAL:

  • Use Drizzle for edge-compatible ORM (works in Cloudflare Workers)
  • Prisma requires Node.js runtime (won't work in Cloudflare Workers)
  • Run migrations from Node.js environment, not from edge functions

Step 5: Query Patterns

CRITICAL - Template Tag Syntax Required:

// ✅ Correct: Template tag syntax (prevents SQL injection)
const users = await sql`SELECT * FROM users WHERE email = ${email}`;

// ❌ Wrong: String concatenation (SQL injection risk)
const users = await sql('SELECT * FROM users WHERE email = ' + email);

Neon Transaction API (Unique Features):

// Automatic transaction (array of queries)
const results = await sql.transaction([
  sql`INSERT INTO users (name) VALUES (${name})`,
  sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${accountId}`
]);

// Manual transaction with callback (for complex logic)
const result = await sql.transaction(async (sql) => {
  const [user] = await sql`INSERT INTO users (name) VALUES (${name}) RETURNING id`;
  await sql`INSERT INTO profiles (user_id) VALUES (${user.id})`;
  return user;
});

Vercel Postgres Transactions:

  • Must use sql.connect() + manual BEGIN/COMMIT/ROLLBACK
  • Always call client.release() in finally block (prevents connection leaks)

Drizzle Transactions:

await db.transaction(async (tx) => {
  await tx.insert(users).values({ name, email });
  await tx.insert(profiles).values({ userId: user.id });
});

Step 6: Handle Connection Pooling

Connection String Format:

Pooled (serverless):     postgresql://user:pass@ep-xyz-pooler.region.aws.neon.tech/db
Non-pooled (direct):     postgresql://user:pass@ep-xyz.region.aws.neon.tech/db

When to Use Each:

  • Pooled (-pooler.): Serverless functions, edge functions, high-concurrency
  • Non-pooled: Long-running servers, migrations, admin tasks, connection limits not a concern

Automatic Pooling (Neon/Vercel):

// Both packages handle pooling automatically when using pooled connection string
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!); // Pooling is automatic

Connection Limits:

  • Neon Free Tier: 100 concurrent connections
  • Pooled Connection: Shares connections across requests
  • Non-Pooled: Each request gets a new connection (exhausts quickly)

CRITICAL:

  • Always use pooled connection strings in serverless environments
  • Non-pooled connections will cause "connection pool exhausted" errors
  • Monitor connection usage in Neon dashboard

Step 7: Deploy and Test

Cloudflare Workers:

// src/index.ts
import { neon } from '@neondatabase/serverless';

export default {
  async fetch(request: Request, env: Env) {
    const sql = neon(env.DATABASE_URL);
    const users = await sql`SELECT * FROM users`;
    return Response.json(users);
  }
};
# Deploy
npx wrangler deploy

Vercel (Next.js API Route):

// app/api/users/route.ts
import { sql } from '@vercel/postgres';

export async function GET() {
  const { rows } = await sql`SELECT * FROM users`;
  return Response.json(rows);
}
# Deploy
vercel deploy --prod

Test Queries:

# Local test
curl http://localhost:8787/api/users

# Production test
curl https://your-app.workers.dev/api/users

Key Points:

  • Test locally before deploying
  • Monitor query performance in Neon dashboard
  • Set up alerts for connection pool exhaustion
  • Use Neon's query history for debugging

Critical Rules (Neon/Vercel-Specific)

✅ MUST DO:

  • Use pooled connection strings (-pooler. in hostname) for serverless
  • Include ?sslmode=require in connection strings
  • Use template tag syntax (sql`...`) to prevent SQL injection
  • Call client.release() in finally block (Vercel Postgres transactions only)
  • Use Drizzle for Cloudflare Workers (Prisma requires Node.js runtime)
  • Use POSTGRES_URL for queries, POSTGRES_PRISMA_URL for Prisma migrations

❌ NEVER DO:

  • Use non-pooled connections or POSTGRES_URL_NON_POOLING in serverless
  • Concatenate SQL strings (use template tags only)
  • Omit sslmode=require (connections will fail)
  • Use Prisma in Cloudflare Workers (V8 isolates don't support it)
  • Run migrations from edge functions (use Node.js environment)

Known Issues Prevention

This skill prevents 19 documented issues:

Issue #1: Connection Pool Exhausted

Error: Error: connection pool exhausted or too many connections for role Source: https://github.com/neondatabase/serverless/issues/12 Why It Happens: Using non-pooled connection string in high-concurrency serverless environment Prevention: Always use pooled connection string (with -pooler. in hostname). Check your connection string format.

Issue #2: TCP Connections Not Supported

Error: Error: TCP connections are not supported in this environment Source: Cloudflare Workers documentation Why It Happens: Traditional Postgres clients use TCP sockets, which aren't available in edge runtimes Prevention: Use @neondatabase/serverless (HTTP/WebSocket-based) instead of pg or postgres.js packages.

Issue #3: SQL Injection from String Concatenation

Error: Successful SQL injection attack or unexpected query results Source: OWASP SQL Injection Guide Why It Happens: Concatenating user input into SQL strings: sql('SELECT * FROM users WHERE id = ' + id) Prevention: Always use template tag syntax: sql`SELECT * FROM users WHERE id = ${id}`. Template tags automatically escape values.

Issue #4: Missing SSL Mode

Error: Error: connection requires SSL or FATAL: no pg_hba.conf entry Source: https://neon.tech/docs/connect/connect-securely Why It Happens: Connection string missing ?sslmode=require parameter Prevention: Always append ?sslmode=require to connection string.

Issue #5: Connection Leak (Vercel Postgres)

Error: Gradually increasing memory usage, eventual timeout errors Source: https://github.com/vercel/storage/issues/45 Why It Happens: Forgetting to call client.release() after manual transactions Prevention: Always use try/finally block and call client.release() in finally block.

Issue #6: Wrong Environment Variable (Vercel)

Error: Error: Connection string is undefined or connect ECONNREFUSED Source: https://vercel.com/docs/storage/vercel-postgres/using-an-orm Why It Happens: Using DATABASE_URL instead of POSTGRES_URL, or vice versa Prevention: Use POSTGRES_URL for queries, POSTGRES_PRISMA_URL for Prisma migrations.

Issue #7: Transaction Timeout in Edge Functions

Error: Error: Query timeout or Error: transaction timeout Source: https://neon.tech/docs/introduction/limits Why It Happens: Long-running transactions exceed edge function timeout (typically 30s) Prevention: Keep transactions short (<5s), batch operations, or move complex transactions to background workers.

Issue #8: Prisma in Cloudflare Workers

Error: Error: PrismaClient is unable to be run in the browser or module resolution errors Source: https://github.com/prisma/prisma/issues/18765 Why It Happens: Prisma requires Node.js runtime with filesystem access Prevention: Use Drizzle ORM for Cloudflare Workers. Prisma works in Vercel Edge/Node.js runtimes only.

Issue #9: Branch API Authentication Error

Error: Error: Unauthorized when calling Neon API Source: https://neon.tech/docs/api/authentication Why It Happens: Missing or invalid NEON_API_KEY environment variable Prevention: Create API key in Neon dashboard → Account Settings → API Keys, set as environment variable.

Issue #10: Stale Connection After Branch Delete

Error: Error: database "xyz" does not exist after deleting a branch Source: https://neon.tech/docs/guides/branching Why It Happens: Application still using connection string from deleted branch Prevention: Update DATABASE_URL when switching branches, restart application after branch changes.

Issue #11: Query Timeout on Cold Start / Auto-Suspend

Error: Error: Query timeout on first request after idle period, or Connection terminated unexpectedly Source: Neon Docs - Auto-suspend | GitHub Issue #168 | Changelog Dec 2025 Why It Happens: Neon auto-suspends compute after ~5 minutes of inactivity (free tier), causing ~1-2s wake-up delay or connection termination Prevention:

  • Set query timeout >= 10s to account for cold starts
  • Use HTTP client (neon()) which handles auto-suspend transparently
  • Handle connection termination errors with Pool:
import { Pool } from '@neondatabase/serverless';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// CRITICAL: Handle connection termination errors
pool.on('error', (err) => {
  console.error('Unexpected database error:', err);
  // Implement reconnection logic or alerting
});
  • Production Configuration: Disable auto-suspend for consistent performance
    • In Neon console: Set minimum compute units > 0
    • Or use compute size >= 16 CU (auto-disables scale-to-zero)
    • Trade-off: Pay for idle time, get consistent <100ms queries
    • Even with auto-suspend disabled, use pooled connection strings for best performance

Issue #12: Drizzle Schema Mismatch

Error: TypeScript errors like Property 'x' does not exist on type 'User' Source: https://orm.drizzle.team/docs/generate Why It Happens: Database schema changed but Drizzle types not regenerated Prevention: Run npx drizzle-kit generate after schema changes, commit generated files.

Issue #13: Migration Conflicts Across Branches

Error: Error: relation "xyz" already exists or migration version conflicts Source: https://neon.tech/docs/guides/branching#schema-migrations Why It Happens: Multiple branches with different migration histories Prevention: Create branches AFTER running migrations on main, or reset branch schema before merging.

Issue #14: PITR Timestamp Out of Range

Error: Error: timestamp is outside retention window Source: https://neon.tech/docs/introduction/point-in-time-restore Why It Happens: Trying to restore from a timestamp older than retention period (7 days on free tier) Prevention: Check retention period for your plan, restore within allowed window.

Issue #15: Wrong Adapter for Prisma

Error: Error: Invalid connection string or slow query performance Source: https://www.prisma.io/docs/orm/overview/databases/neon Why It Happens: Not using @prisma/adapter-neon for serverless environments Prevention: Install @prisma/adapter-neon and @neondatabase/serverless, configure Prisma to use HTTP-based connection.

Issue #16: poolQueryViaFetch Required for Edge Runtimes

Error: WebSocket is not defined or timeout during Next.js 15 prerender with use cache Source: Neon Docs - Prisma Guide | GitHub Issue #181 Why It Happens: Edge runtimes like Cloudflare Workers require HTTP instead of WebSocket for Pool queries. Next.js 15's use cache directive can timeout when using Pool with poolQueryViaFetch. Prevention: Set neonConfig.poolQueryViaFetch = true before using Pool in edge environments.

import { Pool, neonConfig } from '@neondatabase/serverless';

// Enable Pool queries over HTTP fetch (required for edge)
neonConfig.poolQueryViaFetch = true;

const pool = new Pool({ connectionString: env.DATABASE_URL });

export default {
  async fetch(request: Request, env: Env) {
    // Pool.query() now uses HTTP instead of WebSocket
    const result = await pool.query('SELECT * FROM users');
    return Response.json(result.rows);
  }
};

Caveat - Next.js 15 use cache: Avoid poolQueryViaFetch = true with use cache directive - use neon() HTTP client instead:

// ❌ Can timeout during prerender
import { Pool, neonConfig } from '@neondatabase/serverless';
neonConfig.poolQueryViaFetch = true;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

async function getData() {
  'use cache';
  return await pool.query('SELECT * FROM data');
}

// ✅ Works with prerender
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);

async function getData() {
  'use cache';
  return await sql`SELECT * FROM data`;
}

Issue #17: Node v20 Transaction Context Loss with Parallel Operations

Error: Foreign key constraint violations in transactions when using Promise.all(): insert or update on table violates foreign key constraint Source: Drizzle Issue #2200 Why It Happens: When using Node.js v20+ with Neon serverless driver and Drizzle ORM, parallel database operations within a transaction using Promise.all() lose transaction context. Sequential operations work correctly. This is a transaction context management issue specific to Neon driver's session handling in Node v20. Prevention: Use sequential operations or switch to postgres-js driver (not edge-compatible) for Node.js environments.

// ❌ FAILS in Node v20 with Neon driver
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({ name: 'Alice' }).returning();

  // Parallel inserts lose transaction context
  await Promise.all([
    tx.insert(userSettings).values({ userId: user.id, theme: 'dark' }),
    tx.insert(userSettings).values({ userId: user.id, locale: 'en' })
  ]);
  // Error: Foreign key constraint violation (user.id not visible)
});

// ✅ WORKS - Sequential execution
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({ name: 'Alice' }).returning();

  await tx.insert(userSettings).values({ userId: user.id, theme: 'dark' });
  await tx.insert(userSettings).values({ userId: user.id, locale: 'en' });
});

// ✅ ALTERNATIVE - Use postgres-js driver for Node.js (not edge-compatible)
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(connectionString);
const db = drizzle(client);
// Promise.all() works correctly with this driver

Affected Configuration:

  • Node.js: v20.12.2+
  • @neondatabase/serverless: v0.9.0+ (confirmed through v1.0.x)
  • drizzle-orm: v0.30.8+
  • Pattern: Using Promise.all() for parallel inserts in transaction

Issue #18: process.env Access in Sandboxed Runtimes

Error: ReferenceError: process is not defined Source: GitHub Issue #179 Why It Happens: The Neon serverless driver unconditionally accesses process.env.* at the top level, which causes errors in sandboxed runtimes like Slack's Deno runtime that don't provide process.env. Affected Environments: Slack Deno runtime, sandboxed JavaScript environments without Node.js process global Prevention: No workaround available yet. Users must either:

  1. Polyfill process.env in their runtime
  2. Use a different Postgres driver (standard pg with Deno compatibility)
  3. Wait for upstream fix in both @neondatabase/serverless and pg (which also accesses process.env)

Official Status: Open issue with no fix timeline provided. Affects both Neon driver and underlying pg library.


Migration from v0.x to v1.0+

Breaking Change: v1.0.0 requires tagged-template syntax for all SQL queries. Source: Neon Blog Post | GitHub Issue #3678

Before (v0.x):

const result = await sql("SELECT * FROM users WHERE id = $1", [userId]);

After (v1.0+):

// Option 1: Tagged template (recommended)
const result = await sql`SELECT * FROM users WHERE id = ${userId}`;

// Option 2: .query() method for parameterized queries
const result = await sql.query("SELECT * FROM users WHERE id = $1", [userId]);

// Option 3: .unsafe() for trusted raw SQL (dynamic identifiers)
const column = 'name';
const result = await sql`SELECT ${sql.unsafe(column)} FROM users`;

Why this change: The v1.0.0 release enforces tagged-template syntax to prevent SQL injection vulnerabilities. Function-call syntax sql("...", [params]) now throws a runtime error.

Error Message:

This function can now be called only as a tagged-template function:
sql`SELECT ${value}`, not sql("SELECT $1", [value], options)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
53
Last commit
Sep 2026

ahel review

  • S4info
    community integration — published by dennislee928, not postgres

Automated review, not a security audit. Ruleset v1.

Advanced
Catalog kind
skill
Gateway key
neon-vercel-postgres
Source
github.com/dennislee928/ethic-latex