Step 1: Add the Neon MCP Server
bunx add-mcp https://mcp.neon.tech/mcp -ybunx add-mcp https://mcp.neon.tech/mcp -yThis updates all detected agents automatically. If no agents are detected, add -a opencode -a cursor to the command or prompt the user to specify what agents they want to use for this project.
Install the Neon Postgres agent skills so your coding agent knows Neon's operational patterns:
bunx skills add neondatabase/agent-skills -s neon-postgres -s neon-postgres-branches -s neon-postgres-egress-optimizer -a cursor -a codex -ybunx skills add neondatabase/agent-skills -s neon-postgres -s neon-postgres-branches -s neon-postgres-egress-optimizer -a cursor -a codex -y| Skill | Description |
|---|---|
neon-postgres | Neon Postgres setup and operational guidance |
neon-postgres-branches | Create and manage Neon Postgres branches |
neon-postgres-egress-optimizer | Optimize Neon Postgres egress and data transfer |
Step 2: Create a new Neon project
Use an existing Neon project or create a new one, either through the Neon Dashboard or by instructing your coding agent to create a new project or retrieve the connection string of an existing project.
Step 3: Get your Neon database URL
- Go to the Neon Dashboard
- Select your project
- Copy the connection string from the Connection Details widget
- Add it to your
.env.development:
DATABASE_URL="postgresql://user:password@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"DATABASE_URL="postgresql://user:password@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"Then sync to Vercel with bun run env:push. See Environment Variable Management for the full setup.
Tip: Use the pooled connection string for production workloads to improve performance and handle more concurrent connections.
Step 4: Create the database config
Instead of accessing process.env.DATABASE_URL directly, use the type-safe config pattern:
import { configSchema, server } from "better-env/config-schema";
export const databaseConfig = configSchema("Database", {
url: server({ env: "DATABASE_URL" }),
});import { configSchema, server } from "better-env/config-schema";
export const databaseConfig = configSchema("Database", {
url: server({ env: "DATABASE_URL" }),
});Then access via databaseConfig.server.url instead of process.env.DATABASE_URL. See the Environment Variable Management recipe for the full pattern.
Step 5: Validate config on server start
Import the config in instrumentation.ts to validate the environment variable when the server starts:
// Validate required configs on server start
import "./lib/db/config";// Validate required configs on server start
import "./lib/db/config";This ensures the server fails immediately on startup if DATABASE_URL is missing, rather than failing later when a database query runs.
Step 6: Install packages
bun add drizzle-orm pg @vercel/functions
bun add -D drizzle-kit @types/pg @next/envbun add drizzle-orm pg @vercel/functions
bun add -D drizzle-kit @types/pg @next/envThe @next/env package loads environment variables in the same order as Next.js, ensuring your .env.development and .env.local variables are available when running Drizzle Kit commands outside of the Next.js runtime.
Step 7: Create the database client
Create the Drizzle database client:
import { attachDatabasePool } from "@vercel/functions";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { databaseConfig } from "./config";
// Replace with your app's schemas
import * as authSchema from "@/lib/auth/schema";
import * as chatSchema from "@/lib/chat/schema";
const schema = {
...authSchema,
...chatSchema,
};
const pool = new Pool({
connectionString: databaseConfig.server.url,
});
attachDatabasePool(pool);
const db = drizzle({ client: pool, schema });
export { db };import { attachDatabasePool } from "@vercel/functions";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { databaseConfig } from "./config";
// Replace with your app's schemas
import * as authSchema from "@/lib/auth/schema";
import * as chatSchema from "@/lib/chat/schema";
const schema = {
...authSchema,
...chatSchema,
};
const pool = new Pool({
connectionString: databaseConfig.server.url,
});
attachDatabasePool(pool);
const db = drizzle({ client: pool, schema });
export { db };The databaseConfig import provides type-safe access to the DATABASE_URL environment variable. See the Environment Variable Management recipe for the config setup pattern.
Each feature library owns its own schema file (e.g., @/lib/auth/schema, @/lib/chat/schema). Instead of a central db/schema.ts aggregation file, schemas are imported directly in client.ts and merged into a single object for type-safe queries.
Step 8: Configure Drizzle Kit
Create the Drizzle Kit configuration in your project root:
// drizzle.config.ts
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());
import { defineConfig } from "drizzle-kit";
import { databaseConfig } from "./src/lib/db/config";
export default defineConfig({
schema: "./src/lib/*/schema.ts",
out: "./src/lib/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: databaseConfig.server.url,
},
});// drizzle.config.ts
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());
import { defineConfig } from "drizzle-kit";
import { databaseConfig } from "./src/lib/db/config";
export default defineConfig({
schema: "./src/lib/*/schema.ts",
out: "./src/lib/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: databaseConfig.server.url,
},
});The loadEnvConfig call at the top loads environment variables from .env.development, .env.local, and other .env files in the same order as Next.js. This ensures your DATABASE_URL is available when running Drizzle Kit commands like drizzle-kit generate or drizzle-kit migrate.
The schema glob pattern picks up schema.ts files from all feature libraries in src/lib/, following the "everything is a library" pattern where each feature owns its own schema. See Philosophy for more details.
Step 9: Add package.json scripts
Add these scripts to your package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}Step 11: Generate and run migrations
bun run db:generate
bun run db:migratebun run db:generate
bun run db:migrateUnderstanding Connection Pooling
The attachDatabasePool helper from @vercel/functions is the key to efficient database connections on Vercel.
Why it matters:
- Without pooling: Each request opens a new TCP connection (~8 roundtrips), adding latency
- With pooling: The first request establishes a connection; subsequent requests reuse it instantly
- The helper:
attachDatabasePoolensures idle connections close gracefully before function suspension, preventing connection leaks
Info: Alternative Drivers
This recipe uses node-postgres (the pg package) because it provides the best performance on Vercel with Fluid compute. However, Drizzle supports other Postgres drivers:
| Driver | When to consider |
|---|---|
| postgres.js | If you prefer its API or need specific features like tagged template queries |
| Neon Serverless | For platforms without connection pooling (Netlify, Deno Deploy, Cloudflare Workers) |
Note: If you're deploying to a serverless platform that doesn't support connection pooling, the Neon Serverless driver connects over HTTP (~3 roundtrips) instead of TCP (~8 roundtrips), which is faster for single queries in classic serverless environments.