Loading...
Loading...
Set up serverless Postgres with Neon or Vercel Postgres for Cloudflare Workers/Edge. Includes connection pooling, git-like branching, and Drizzle ORM integration. Use when: setting up edge Postgres, troubleshooting "TCP not supported", connection pool exhausted, SSL config errors, or Node v20 transaction issues.
npx skill4agent add jezweb/claude-skills neon-vercel-postgres@neondatabase/serverless@1.0.2@vercel/postgres@0.10.0drizzle-orm@0.45.1drizzle-kit@0.31.8neonctl@2.19.0npm install @neondatabase/serverlessnpm install @vercel/postgres# 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# In your Vercel project
vercel postgres create
vercel env pull .env.local # Automatically creates POSTGRES_URL and other vars-pooler.region.aws.neon.tech?sslmode=requireimport { 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}`
]);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();
}sql`...`sql('SELECT * FROM users WHERE id = ' + id)npm install @neondatabase/serverlessnpm install @vercel/postgres# 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/serverlesspostgresql://user:pass@ep-xyz-pooler.region.aws.neon.tech/db?sslmode=requirevercel env pull# 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-pooler.region.aws.neon.tech?sslmode=require# .env or .env.local
DATABASE_URL="postgresql://user:password@ep-xyz-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"# 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="..."{
"vars": {
"DATABASE_URL": "postgresql://user:password@ep-xyz-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"
}
}POSTGRES_URLPOSTGRES_PRISMA_URLPOSTGRES_URL_NON_POOLING// 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()
)
`;// 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// 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// ✅ 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);// 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;
});sql.connect()BEGINCOMMITROLLBACKclient.release()finallyawait db.transaction(async (tx) => {
await tx.insert(users).values({ name, email });
await tx.insert(profiles).values({ userId: user.id });
});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-pooler.// 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// 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// 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# Local test
curl http://localhost:8787/api/users
# Production test
curl https://your-app.workers.dev/api/users-pooler.?sslmode=requiresql`...`client.release()finallyPOSTGRES_URLPOSTGRES_PRISMA_URLPOSTGRES_URL_NON_POOLINGsslmode=requireError: connection pool exhaustedtoo many connections for role-pooler.Error: TCP connections are not supported in this environment@neondatabase/serverlesspgpostgres.jssql('SELECT * FROM users WHERE id = ' + id)sql`SELECT * FROM users WHERE id = ${id}`Error: connection requires SSLFATAL: no pg_hba.conf entry?sslmode=require?sslmode=requireclient.release()client.release()Error: Connection string is undefinedconnect ECONNREFUSEDDATABASE_URLPOSTGRES_URLPOSTGRES_URLPOSTGRES_PRISMA_URLError: Query timeoutError: transaction timeoutError: PrismaClient is unable to be run in the browserError: UnauthorizedNEON_API_KEYError: database "xyz" does not existDATABASE_URLError: Query timeoutConnection terminated unexpectedlyneon()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
});Property 'x' does not exist on type 'User'npx drizzle-kit generateError: relation "xyz" already existsError: timestamp is outside retention windowError: Invalid connection string@prisma/adapter-neon@prisma/adapter-neon@neondatabase/serverlessWebSocket is not defineduse cacheuse cachepoolQueryViaFetchneonConfig.poolQueryViaFetch = trueimport { 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);
}
};use cachepoolQueryViaFetch = trueuse cacheneon()// ❌ 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`;
}Promise.all()insert or update on table violates foreign key constraintPromise.all()// ❌ 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 driverPromise.all()ReferenceError: process is not definedprocess.env.*process.envprocess.envpg@neondatabase/serverlesspgconst result = await sql("SELECT * FROM users WHERE id = $1", [userId]);// 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`;sql("...", [params])This function can now be called only as a tagged-template function:
sql`SELECT ${value}`, not sql("SELECT $1", [value], options)sql("...", [params]){
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
"better-auth": "^1.3.4",
"drizzle-orm": "^0.40.1"
}
}import { Kysely } from 'kysely';
import { Pool } from '@neondatabase/serverless';
const db = new Kysely({
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL })
})
});| Use Case | Recommended | Reason |
|---|---|---|
| Single query per request | HTTP ( | Lower initial latency (~37ms) |
| 2+ sequential queries | WebSocket ( | Lower per-query latency (~5ms) |
| Parallel independent queries | HTTP | Better parallelization |
| Interactive transactions | WebSocket (required) | Required for transaction context |
| Edge Functions (single-shot) | HTTP | No connection overhead |
| Long-running workers | WebSocket | Amortize connection cost |
// HTTP: Best for single queries
import { neon } from '@neondatabase/serverless';
const sql = neon(env.DATABASE_URL);
const users = await sql`SELECT * FROM users`; // ~37ms
// WebSocket: Best for multiple sequential queries
import { Pool } from '@neondatabase/serverless';
const pool = new Pool({ connectionString: env.DATABASE_URL });
const client = await pool.connect(); // ~15ms setup
try {
const user = await client.query('SELECT * FROM users WHERE id = $1', [1]); // ~5ms
const posts = await client.query('SELECT * FROM posts WHERE user_id = $1', [1]); // ~5ms
const comments = await client.query('SELECT * FROM comments WHERE user_id = $1', [1]); // ~5ms
// Total: ~30ms (vs ~111ms with HTTP)
} finally {
client.release();
}{
"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!
}
});@neondatabase/serverless@vercel/postgresdrizzle-ormdrizzle-kitimport { 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));# 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_URLchmod +x scripts/setup-neon.sh
./scripts/setup-neon.sh my-project-namenpx tsx scripts/test-connection.tsreferences/connection-strings.mdreferences/drizzle-setup.mdreferences/prisma-setup.mdreferences/branching-guide.mdreferences/migration-strategies.mdreferences/common-errors.mdconnection-strings.mddrizzle-setup.mdprisma-setup.mdbranching-guide.mdcommon-errors.mdassets/schema-example.sqlassets/drizzle-schema.tsassets/prisma-schema.prisma# 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@neondatabase/serverless@^1.0.2@vercel/postgres@^0.10.0drizzle-orm@^0.44.7drizzle-kit@^0.31.7@prisma/client@^6.10.0@prisma/adapter-neon@^6.10.0neonctl@^2.19.0zod@^3.24.0/github/neondatabase/serverless/github/vercel/storage{
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
"@vercel/postgres": "^0.10.0",
"drizzle-orm": "^0.45.1"
},
"devDependencies": {
"drizzle-kit": "^0.31.8",
"neonctl": "^2.19.0"
}
}{
"dependencies": {
"@prisma/client": "^6.10.0",
"@prisma/adapter-neon": "^6.10.0"
},
"devDependencies": {
"prisma": "^6.10.0"
}
}Error: connection pool exhausted-pooler.region.aws.neon.techError: TCP connections are not supported@neondatabase/serverlesspgpostgres.jsError: database "xyz" does not existDATABASE_URLPrismaClient is unable to be run in the browser@prisma/adapter-neonneonctl branches reset feature --parent mainWarning: @neondatabase/serverless can only connect to remote Neon/Vercel Postgres/Supabase instances through a websocketpgNeonDbError: Error connecting to database: fetch failed [cause]: SocketError: other side closed# Whitelist these domains in VPN:
*.neon.tech
*.aws.neon.tech@neondatabase/serverless@vercel/postgres-pooler.?sslmode=requireDATABASE_URLPOSTGRES_URLsql`...`references/common-errors.mdsslmode=requirescripts/test-connection.ts