Loading...
Loading...
This skill provides comprehensive knowledge for integrating Neon serverless Postgres and Vercel Postgres (which is built on Neon infrastructure) into web applications. It should be used when setting up serverless Postgres databases, configuring connection pooling for edge and serverless environments, implementing database branching workflows, or troubleshooting Postgres connection issues in Cloudflare Workers, Vercel Edge Functions, or Node.js serverless functions. Use this skill when: - Setting up Neon Postgres for Cloudflare Workers, Vercel Edge, or serverless environments - Configuring Vercel Postgres for Next.js applications - Implementing database branching workflows (git-like database branches) - Integrating Drizzle ORM or Prisma with Neon/Vercel Postgres - Debugging connection pool errors, transaction timeouts, or SSL configuration issues - Migrating from D1/SQLite to Postgres or from traditional Postgres to serverless Postgres - Setting up point-in-time restore (PITR) or database backups - Encountering errors like "connection pool exhausted", "TCP connections not supported in serverless", or "sslmode required" Keywords: neon postgres, @neondatabase/serverless, @vercel/postgres, serverless postgres, postgres edge, neon branching, vercel database, http postgres, websocket postgres, pooled connection, drizzle neon, prisma neon, postgres cloudflare, postgres vercel edge, sql template tag, neonctl, database branches, point in time restore, postgres migrations, serverless sql, edge database, neon api, vercel sql
npx skill4agent add jackspace/claudeskillz neon-vercel-postgres@neondatabase/serverless@1.0.2@vercel/postgres@0.10.0drizzle-orm@0.44.7neonctl@2.16.1npm 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)
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
# Prisma (alternative)
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
# Authenticate
neonctl auth
# Create project
neonctl projects create --name my-app
# Get connection string
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 initimport { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
// SELECT
const users = await sql`SELECT * FROM users WHERE email = ${email}`;
// INSERT
const newUser = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
// UPDATE
await sql`UPDATE users SET name = ${newName} WHERE id = ${id}`;
// DELETE
await sql`DELETE FROM users WHERE id = ${id}`;import { sql } from '@vercel/postgres';
// SELECT
const { rows } = await sql`SELECT * FROM users WHERE email = ${email}`;
// INSERT
const { rows: newUser } = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;// Automatic transaction
const results = await sql.transaction([
sql`INSERT INTO users (name) VALUES (${name})`,
sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${accountId}`
]);
// Manual transaction (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;
});import { sql } from '@vercel/postgres';
const client = await sql.connect();
try {
await client.sql`BEGIN`;
const { rows } = await client.sql`INSERT INTO users (name) VALUES (${name}) RETURNING id`;
await client.sql`INSERT INTO profiles (user_id) VALUES (${rows[0].id})`;
await client.sql`COMMIT`;
} catch (e) {
await client.sql`ROLLBACK`;
throw e;
} finally {
client.release();
}import { db } from './db';
import { users } from './db/schema';
import { eq } from 'drizzle-orm';
// SELECT
const allUsers = await db.select().from(users);
const user = await db.select().from(users).where(eq(users.email, email));
// INSERT
const newUser = await db.insert(users).values({ name, email }).returning();
// UPDATE
await db.update(users).set({ name: newName }).where(eq(users.id, id));
// DELETE
await db.delete(users).where(eq(users.id, id));
// Transactions
await db.transaction(async (tx) => {
await tx.insert(users).values({ name, email });
await tx.insert(profiles).values({ userId: user.id });
});sql`...`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.sql`SELECT * FROM users`sslmode=require'SELECT * FROM users WHERE id = ' + idsslmode=requireclient.release().env.gitignorePOSTGRES_URL_NON_POOLINGError: 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 timeoutProperty '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/serverless{
"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.0"
},
"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-kit// src/index.ts
import { neon } from '@neondatabase/serverless';
interface Env {
DATABASE_URL: string;
}
export default {
async fetch(request: Request, env: Env) {
const sql = neon(env.DATABASE_URL);
// Parse request
const url = new URL(request.url);
if (url.pathname === '/api/users' && request.method === 'GET') {
const users = await sql`SELECT id, name, email FROM users`;
return Response.json(users);
}
if (url.pathname === '/api/users' && request.method === 'POST') {
const { name, email } = await request.json();
const [user] = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
return Response.json(user, { status: 201 });
}
return new Response('Not Found', { status: 404 });
}
};// app/actions/users.ts
'use server';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
export async function getUsers() {
const { rows } = await sql`SELECT id, name, email FROM users ORDER BY created_at DESC`;
return rows;
}
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const { rows } = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
revalidatePath('/users');
return rows[0];
}
export async function deleteUser(id: number) {
await sql`DELETE FROM users WHERE id = ${id}`;
revalidatePath('/users');
}// db/schema.ts
import { pgTable, serial, text, timestamp, integer } 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()
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id),
title: text('title').notNull(),
content: text('content'),
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 });// app/api/posts/route.ts
import { db } from '@/db';
import { posts, users } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function GET() {
// Type-safe query with joins
const postsWithAuthors = await db
.select({
postId: posts.id,
title: posts.title,
content: posts.content,
authorName: users.name
})
.from(posts)
.leftJoin(users, eq(posts.userId, users.id));
return Response.json(postsWithAuthors);
}// Neon Direct - Automatic Transaction
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const result = await sql.transaction(async (tx) => {
// Deduct from sender
const [sender] = await tx`
UPDATE accounts
SET balance = balance - ${amount}
WHERE id = ${senderId} AND balance >= ${amount}
RETURNING *
`;
if (!sender) {
throw new Error('Insufficient funds');
}
// Add to recipient
await tx`
UPDATE accounts
SET balance = balance + ${amount}
WHERE id = ${recipientId}
`;
// Log transaction
await tx`
INSERT INTO transfers (from_id, to_id, amount)
VALUES (${senderId}, ${recipientId}, ${amount})
`;
return sender;
});# 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 from main
neonctl branches create --name dev --parent main
# Create from specific point in time (PITR)
neonctl branches create --name restore-point --parent main --timestamp "2025-10-28T10:00:00Z"
# Create from another branch
neonctl branches create --name feature --parent dev# List branches
neonctl branches list
# Get connection string
neonctl connection-string dev
# Delete branch
neonctl branches delete feature
# Reset branch to match parent
neonctl branches reset dev --parent main| Feature | Pooled ( | Non-Pooled |
|---|---|---|
| Use Case | Serverless, edge functions | Long-running servers |
| Max Connections | ~10,000 (shared) | ~100 (per database) |
| Connection Reuse | Yes | No |
| Latency | +1-2ms overhead | Direct |
| Idle Timeout | 60s | Configurable |
Error: connection pool exhaustedconst result = await sql`
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = ${email}
`;await sql`CREATE INDEX idx_users_email ON users(email)`;
await sql`CREATE INDEX idx_posts_user_id ON posts(user_id)`;import { pgTable, serial, text, index } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique()
}, (table) => ({
emailIdx: index('email_idx').on(table.email)
}));// ❌ Bad: N+1 queries
for (const user of users) {
const posts = await sql`SELECT * FROM posts WHERE user_id = ${user.id}`;
}
// ✅ Good: Single query with JOIN
const postsWithUsers = await sql`
SELECT users.*, posts.*
FROM users
LEFT JOIN posts ON posts.user_id = users.id
`;const getUserByEmail = db.select().from(users).where(eq(users.email, sql.placeholder('email'))).prepare('get_user_by_email');
// Reuse prepared statement
const user1 = await getUserByEmail.execute({ email: 'alice@example.com' });
const user2 = await getUserByEmail.execute({ email: 'bob@example.com' });// ❌ Bad
const sql = neon('postgresql://user:pass@host/db');
// ✅ Good
const sql = neon(process.env.DATABASE_URL!);-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Create policy
CREATE POLICY "Users can only see their own posts"
ON posts
FOR SELECT
USING (user_id = current_user_id());// ✅ Validate before query
const emailSchema = z.string().email();
const email = emailSchema.parse(input.email);
const user = await sql`SELECT * FROM users WHERE email = ${email}`;// ✅ Always paginate
const page = Math.max(1, parseInt(request.query.page));
const limit = 50;
const offset = (page - 1) * limit;
const users = await sql`
SELECT * FROM users
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;CREATE ROLE readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;@neondatabase/serverless@^1.0.2@vercel/postgres@^0.10.0drizzle-orm@^0.44.7drizzle-kit@^0.31.0@prisma/client@^6.10.0@prisma/adapter-neon@^6.10.0neonctl@^2.16.1zod@^3.24.0/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.0",
"neonctl": "^2.16.1"
}
}{
"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 main@neondatabase/serverless@vercel/postgres-pooler.?sslmode=requireDATABASE_URLPOSTGRES_URLsql`...`references/common-errors.mdsslmode=requirescripts/test-connection.ts