Connect Your Database
Wire up Neon Neon Serverless PostgreSQL. It auto-scales, scales to zero when idle, branches like Git, and has a free tier (as of Sep 2026). You can provision it straight from the Vercel Marketplace. Perfect for vibe coding. "Like PostgreSQL that wakes up when you need it and sleeps when you don't. Pay for what you use." Drizzle ORM A lightweight, type-safe ORM for TypeScript. Your schema IS your types — no code generation, no sync issues. SQL-like syntax that feels natural. "Like having a personal translator who speaks both TypeScript and SQL fluently. Zero confusion." Migration A controlled change to your database schema. Lets you version-control your database structure and safely update it. "Like renovating a house room by room, with blueprints for each change."
The Stack
Neon
Serverless Postgres. Free plan, instant provisioning, branching.
Drizzle ORM
Type-safe SQL. Schema in TypeScript, generated migrations, zero bloat.
Next.js
Server Components Server Component A React component that runs ONLY on the server. Can directly access databases, fetch data, and keeps secrets safe. The default in Next.js App Router. "Like the kitchen in a restaurant. Customers never see it, but that's where the magic happens."
Before You Start
- ✓A Next.js app (follow Your First App if needed) on a current Node LTS (Node 24 recommended)
- ✓Either a Vercel project you can link, or a free Neon account at neon.com
Create a Neon Database
Pick one route. Both end with a connection string in .env.local.
Route A: Vercel Marketplace
Installs Neon, connects it to your project, and writes the env vars to .env.local. Preview deployments can get their own database branch.
Terminal
vercel link
vercel install neonRoute B: Neon Console
- 1. Sign in at console.neon.tech
- 2. Create a project and pick the region closest to your Vercel functions (Vercel's default region is
iad1, US East) - 3. Click Connect and copy the pooled connection string
A pooled Neon connection string has -pooler in the hostname:
Connection string
postgresql://username:password@ep-xxx-pooler.us-east-2.aws.neon.tech/neondb?sslmode=requireKeep this secret!
The string contains your password. Never commit it to Git. It lives in an environment variable Environment Variable A secret value stored outside your code, like API keys or passwords. Keeps sensitive info out of your codebase. "Like a sticky note with the WiFi password — you know it, but you don't write it on the wall."
Install Dependencies
Install Drizzle, the Neon serverless driver, and dotenv (so Drizzle Kit can read .env.local):
Terminal
npm install drizzle-orm @neondatabase/serverless dotenvAnd Drizzle Kit for migrations (dev dependency):
Terminal
npm install -D drizzle-kitSet Up Environment Variables
If you used Route A, run vercel env pull and skip ahead. Otherwise create .env.local in your project root:
.env.local
DATABASE_URL="postgresql://username:password@ep-xxx-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"Don't forget .gitignore
create-next-app's .gitignore already covers .env* files. Double-check before your first commit. More in the env vars guide.
Create the Database Connection
Create lib/db.ts. The neon-http driver sends each query over HTTPS, so there are no long-lived connections for serverless functions to exhaust:
lib/db.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql });You now have a db object ready for queries. (Need interactive transactions? Switch to the WebSocket driver, drizzle-orm/neon-serverless, later. HTTP is the right default.)
Define Your Schema
Create lib/schema.ts to define your tables:
lib/schema.ts
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
content: text("content"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// TypeScript types for a post, inferred from the table
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;Why Drizzle?
Your schema Schema The structure of your database — what tables exist, what columns they have, and how they relate to each other. "Like the blueprint of a building. It defines the shape before you add the furniture (data)."
Configure Drizzle Kit
Create drizzle.config.ts in your project root:
drizzle.config.ts
import { defineConfig } from "drizzle-kit";
import { config } from "dotenv";
config({ path: ".env.local" });
export default defineConfig({
schema: "./lib/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Tip: Neon recommends a direct (unpooled) connection for schema changes. If you have one (same string without -pooler), store it as something like DATABASE_URL_UNPOOLED and use it here instead.
Push Schema to Database
For a brand-new dev database, push the schema straight in:
Terminal
npx drizzle-kit pushYou should see output confirming the posts table was created.
push vs. generate + migrate
push is great for prototyping. Once real users exist, switch to migration files you can review and commit:
Terminal
npx drizzle-kit generate # write SQL migration files to ./drizzle
npx drizzle-kit migrate # apply them to the database
npx drizzle-kit studio # visual database browserQuery Data in Your App
Server Components can fetch data directly. Update app/page.tsx:
app/page.tsx
import { db } from "@/lib/db";
import { posts } from "@/lib/schema";
export default async function Home() {
// Runs on the server. No API route needed!
const allPosts = await db.select().from(posts);
return (
<main className="min-h-screen p-8 bg-black text-white">
<h1 className="text-4xl font-bold mb-8">My Posts</h1>
{allPosts.length === 0 ? (
<p className="text-gray-400">No posts yet.</p>
) : (
<ul className="space-y-4">
{allPosts.map((post) => (
<li key={post.id} className="p-4 bg-gray-900 rounded-lg">
<h2 className="text-xl font-bold">{post.title}</h2>
<p className="text-gray-400">{post.content}</p>
</li>
))}
</ul>
)}
</main>
);
}No "use client" needed!
Server Components can await database calls directly. The query runs on the server and only HTML ships to the browser, so your DATABASE_URL never leaves the server.
Add Some Test Data
Create a small API route to add posts at app/api/posts/route.ts:
app/api/posts/route.ts
import { db } from "@/lib/db";
import { posts } from "@/lib/schema";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { title, content } = await request.json();
if (typeof title !== "string" || title.length === 0) {
return NextResponse.json({ error: "title is required" }, { status: 400 });
}
const [newPost] = await db
.insert(posts)
.values({ title, content })
.returning();
return NextResponse.json(newPost, { status: 201 });
}Test it with curl:
Terminal
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "Hello World", "content": "My first post!"}'Refresh your homepage. Your post should appear! Before you ship, put this route behind authentication so strangers can't write to your database.
Deploy
On Vercel, the app reads DATABASE_URL from project env vars. Route A already set them. For Route B, add it yourself:
Terminal
vercel env add DATABASE_URL production
vercel --prodDatabase Connected!
You're fetching real data from Postgres with full TypeScript safety. Just SQL, no vendor SDK lock-in.
Final File Structure
Common Issues
"DATABASE_URL is not defined"
Make sure .env.local exists (or run vercel env pull) and restart the dev server after creating it.
"relation does not exist"
The table isn't in this database yet. Run npx drizzle-kit push, and check you're pointed at the right branch.
First request is slow
Neon suspends compute after 5 minutes of inactivity and wakes it on the next query. That first query takes a moment longer. Normal on the free plan.
"Too many connections"
You're probably using a TCP driver with the direct string from many serverless instances. Use the neon-http driver or the -pooler connection string.
Types not working?
Import from @/lib/schema (with the @ alias) and check tsconfig.json has the @/* path alias.
Quick Query Reference
Select All
const all = await db.select().from(posts);Select with Filter
import { eq } from "drizzle-orm";
const post = await db.select().from(posts).where(eq(posts.id, 1));Insert
await db.insert(posts).values({ title: "New Post", content: "..." });Update
await db.update(posts).set({ title: "Updated" }).where(eq(posts.id, 1));Delete
await db.delete(posts).where(eq(posts.id, 1));Next Steps
Neon Playbook
Branching, connection pooling, and Neon + Claude Code.
Read PlaybookDrizzle Deep Dive
Relations, migrations workflow, and query patterns.
Read GuideAdd Authentication
Protect your data with user accounts and login flows.
Read PlaybookDrizzle Docs
Full documentation for advanced queries, relations, and more.
Visit Site