Set up serverless Postgres with Neon or Vercel Postgres for Cloudflare Workers/Edge. Includes connection pooling, git-like branching for preview environments, and Drizzle/Prisma integration. Use when: setting up edge Postgres, configuring database branching, or troubleshooting "TCP not supported", connection pool exhausted, SSL config (sslmode=require), or Prisma edge compatibility.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdassets/drizzle-schema.tsassets/example-template.txtreferences/example-reference.mdscripts/example-script.shscripts/test-connection.tstemplates/drizzle-migrations-workflow.mdtemplates/drizzle-queries.tstemplates/drizzle-schema.tstemplates/neon-basic-queries.tstemplates/package.jsonname: neon-vercel-postgres description: | Set up serverless Postgres with Neon or Vercel Postgres for Cloudflare Workers/Edge. Includes connection pooling, git-like branching for preview environments, and Drizzle/Prisma integration.
Status: Production Ready
Last Updated: 2025-11-26
Dependencies: None
Latest Versions: @neondatabase/serverless@1.0.2, @vercel/postgres@0.10.0, drizzle-orm@0.44.7, drizzle-kit@0.31.7, neonctl@2.18.1
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:
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:
-pooler.region.aws.neon.tech)?sslmode=require parameterNeon 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:
sql`...`) for automatic SQL injection protectionsql('SELECT * FROM users WHERE id = ' + id) ❌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.44.7 @neondatabase/serverless@1.0.2
npm install -D drizzle-kit@0.31.7
# Prisma (Node.js only)
npm install prisma @prisma/client @prisma/adapter-neon @neondatabase/serverless
Key Points:
Option A: Neon Dashboard
postgresql://user:pass@ep-xyz-pooler.region.aws.neon.tech/db?sslmode=requireOption B: Vercel Dashboard
vercel env pull to get environment variables locallyOption C: Neon CLI (neonctl@2.18.1)
# Install CLI
npm install -g neonctl@2.18.1
# Authenticate
neonctl auth
# Create project and get connection string
neonctl projects create --name my-app
neonctl connection-string main
CRITICAL:
-pooler.region.aws.neon.tech)?sslmode=require in connection stringFor 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:
POSTGRES_URL (pooled) for queriesPOSTGRES_PRISMA_URL for Prisma migrationsPOSTGRES_URL_NON_POOLING in serverless functionsOption 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:
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:
sql.connect() + manual BEGIN/COMMIT/ROLLBACKclient.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 });
});
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:
-pooler.): Serverless functions, edge functions, high-concurrencyAutomatic 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:
CRITICAL:
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:
✅ MUST DO:
-pooler. in hostname) for serverless?sslmode=require in connection stringssql`...`) to prevent SQL injectionclient.release() in finally block (Vercel Postgres transactions only)POSTGRES_URL for queries, POSTGRES_PRISMA_URL for Prisma migrations❌ NEVER DO:
POSTGRES_URL_NON_POOLING in serverlesssslmode=require (connections will fail)This skill prevents 15 documented issues:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Error: Error: Query timeout on first request after idle period
Source: https://neon.tech/docs/introduction/auto-suspend
Why It Happens: Neon auto-suspends compute after inactivity, ~1-2s to wake up
Prevention: Expect cold starts, set query timeout >= 10s, or disable auto-suspend (paid plans).
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.
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.
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.
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.
{
"dependencies": {
"@neondatabase/serverless": "^1.0.2"
}
}
{
"dependencies": {
"@vercel/postgres": "^0.10.0"
}
}
{
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
"drizzle-orm": "^0.44.7"
},
"devDependencies": {
"drizzle-kit": "^0.31.7"
},
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/schema.ts',
out: './db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!
}
});
Why these settings:
@neondatabase/serverless is edge-compatible (HTTP/WebSocket-based)@vercel/postgres provides zero-config on Verceldrizzle-orm works in all runtimes (Cloudflare Workers, Vercel Edge, Node.js)drizzle-kit handles migrations and schema generationimport { neon } from '@neondatabase/serverless';
interface Env { DATABASE_URL: string; }
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);
}
};
'use server';
import { sql } from '@vercel/postgres';
export async function getUsers() {
const { rows } = await sql`SELECT * FROM users`;
return rows;
}
// 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 });
// Usage: Type-safe queries with JOINs
const postsWithAuthors = await db
.select({ postId: posts.id, authorName: users.name })
.from(posts)
.leftJoin(users, eq(posts.userId, users.id));
See Step 5 for Neon's unique transaction API (array syntax or callback syntax)
# Create branch for PR
neonctl branches create --project-id my-project --name pr-123 --parent main
# Get connection string for branch
BRANCH_URL=$(neonctl connection-string pr-123)
# Use in Vercel preview deployment
vercel env add DATABASE_URL preview
# Paste $BRANCH_URL
# Delete branch when PR is merged
neonctl branches delete pr-123
# .github/workflows/preview.yml
name: Create Preview Database
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Create Neon Branch
run: |
BRANCH_NAME="pr-${{ github.event.pull_request.number }}"
neonctl branches create --project-id ${{ secrets.NEON_PROJECT_ID }} --name $BRANCH_NAME
BRANCH_URL=$(neonctl connection-string $BRANCH_NAME)
- name: Deploy to Vercel
env:
DATABASE_URL: ${{ steps.branch.outputs.url }}
run: vercel deploy --env DATABASE_URL=$DATABASE_URL
When to use: Want isolated database for each PR/preview deployment
setup-neon.sh - Creates Neon database and outputs connection string
chmod +x scripts/setup-neon.sh
./scripts/setup-neon.sh my-project-name
test-connection.ts - Verifies database connection and runs test query
npx tsx scripts/test-connection.ts
references/connection-strings.md - Complete guide to connection string formats, pooled vs non-pooledreferences/drizzle-setup.md - Step-by-step Drizzle ORM setup with Neonreferences/prisma-setup.md - Prisma setup with Neon adapterreferences/branching-guide.md - Comprehensive guide to Neon database branchingreferences/migration-strategies.md - Migration patterns for different ORMs and toolsreferences/common-errors.md - Extended troubleshooting guideWhen Claude should load these:
connection-strings.md when debugging connection issuesdrizzle-setup.md when user wants to use Drizzle ORMprisma-setup.md when user wants to use Prismabranching-guide.md when user asks about preview environments or database branchingcommon-errors.md when encountering specific error messagesassets/schema-example.sql - Example database schema with users, posts, commentsassets/drizzle-schema.ts - Complete Drizzle schema templateassets/prisma-schema.prisma - Complete Prisma schema templateNeon provides git-like database branching:
# Create branch from main
neonctl branches create --name dev --parent main
# Create from point-in-time (PITR restore)
neonctl branches create --name restore --parent main --timestamp "2025-10-28T10:00:00Z"
# Get connection string for branch
neonctl connection-string dev
# Delete branch
neonctl branches delete feature
Key Features:
Connection Pool Monitoring:
Query Optimization:
Security:
Required:
@neondatabase/serverless@^1.0.2 - Neon serverless Postgres client (HTTP/WebSocket-based)@vercel/postgres@^0.10.0 - Vercel Postgres client (alternative to Neon direct, Vercel-specific)Optional:
drizzle-orm@^0.44.7 - TypeScript ORM (edge-compatible, recommended)drizzle-kit@^0.31.7 - Drizzle schema migrations and introspection@prisma/client@^6.10.0 - Prisma ORM (Node.js only, not edge-compatible)@prisma/adapter-neon@^6.10.0 - Prisma adapter for Neon serverlessneonctl@^2.18.1 - Neon CLI for database managementzod@^3.24.0 - Schema validation for input sanitization/github/neondatabase/serverless, /github/vercel/storage{
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
"@vercel/postgres": "^0.10.0",
"drizzle-orm": "^0.44.7"
},
"devDependencies": {
"drizzle-kit": "^0.31.7",
"neonctl": "^2.18.1"
}
}
Latest Prisma (if needed):
{
"dependencies": {
"@prisma/client": "^6.10.0",
"@prisma/adapter-neon": "^6.10.0"
},
"devDependencies": {
"prisma": "^6.10.0"
}
}
This skill is based on production deployments of Neon and Vercel Postgres:
Error: connection pool exhaustedSolution:
-pooler.region.aws.neon.tech)Error: TCP connections are not supportedSolution:
@neondatabase/serverless instead of pg or postgres.jsError: database "xyz" does not existSolution:
DATABASE_URL points to correct databaseSolution:
PrismaClient is unable to be run in the browserSolution:
@prisma/adapter-neonSolution:
neonctl branches reset feature --parent mainUse this checklist to verify your setup:
@neondatabase/serverless or @vercel/postgres)-pooler.)?sslmode=requireDATABASE_URL or POSTGRES_URL)sql`...`)Questions? Issues?
references/common-errors.md for extended troubleshootingsslmode=require is in connection stringscripts/test-connection.ts