Most Next.js apps on Vercel eventually need a database. The answer in 2026 is usually Neon — serverless Postgres that scales to zero, has database branching, and integrates with Vercel in one click.
But there's one mistake almost everyone makes when setting it up. It causes random connection failures in production that are hell to debug. Let's cover that first.
The #1 mistake: using the wrong connection string
Neon gives you two connection strings. Most people grab the first one and use it everywhere. That's the mistake.
# Pooled connection (PgBouncer) — for your app
DATABASE_URL=postgresql://user:pass@ep-xxxx-pooler.us-east-2.aws.neon.tech/dbname
# Direct connection — for migrations ONLY
DATABASE_URL_UNPOOLED=postgresql://user:pass@ep-xxxx.us-east-2.aws.neon.tech/dbname
Use DATABASE_URL (pooled) for your application code. This goes through PgBouncer, which handles connection pooling for serverless environments where your function creates a new connection on every invocation.
Use DATABASE_URL_UNPOOLED (direct) only for Drizzle migrations. Drizzle needs a persistent connection to run migrations — PgBouncer breaks this.
If you use the direct connection for your app, you'll exhaust Neon's connection limit under load. If you use the pooled connection for migrations, they'll fail randomly.
Setup: Neon + Next.js 15 + Drizzle
1. Install dependencies
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit dotenv
2. Configure Drizzle for Neon
// lib/db.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
The neon-http driver is designed for Edge and Serverless. It uses HTTP requests instead of WebSockets, which means no persistent connection — perfect for Vercel Functions and Next.js.
3. Define your schema
// lib/schema.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: uuid('author_id').references(() => users.id).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
4. Configure drizzle.config.ts
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './lib/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL_UNPOOLED!, // Direct connection for migrations
},
});
5. Add migration scripts
// package.json
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}
Using it in Next.js Server Components
// app/posts/page.tsx
import { db } from '@/lib/db';
import { posts, users } from '@/lib/schema';
import { eq } from 'drizzle-orm';
export default async function PostsPage() {
const allPosts = await db
.select({
id: posts.id,
title: posts.title,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.orderBy(posts.createdAt);
return (
<ul>
{allPosts.map(post => (
<li key={post.id}>{post.title} — {post.authorName}</li>
))}
</ul>
);
}
No 'use server' needed — this is a Server Component and runs on the server by default.
Database branching for preview environments
This is Neon's killer feature. Every branch (in git) gets its own database branch — isolated, with its own data, pointing to the same schema.
With the Vercel integration enabled, Neon creates a database branch automatically for every PR. The preview deployment gets its own isolated database.
Practical workflow:
- Create a feature branch in git
- Neon auto-creates a matching database branch with a copy of your schema
- Your preview deployment connects to that branch's DATABASE_URL
- You can migrate and test without touching production data
- Branch gets deleted when the PR closes
For teams, this is transformative. No more "don't test on production" — every PR gets its own safe playground.
Neon vs Supabase in 2026
| Neon | Supabase | |
|---|---|---|
| Scale to zero | ✅ | ✅ (paid only) |
| Database branching | ✅ | ❌ |
| Serverless native | ✅ | Partial |
| Built-in auth | ❌ | ✅ |
| Storage | ❌ | ✅ |
| Edge runtime | ✅ | Partial |
| Free tier storage | 512 MB | 500 MB |
Choose Neon if: you just need Postgres, you deploy to serverless/edge, or you want database branching for PRs.
Choose Supabase if: you want auth + storage + realtime built in without wiring things together yourself.
Production checklist
- [ ]
DATABASE_URL(pooled) in app env vars - [ ]
DATABASE_URL_UNPOOLED(direct) in CI/CD for migrations only - [ ] Migrations run before deployment, not after
- [ ] Connection pooling set to
transactionmode in Neon dashboard - [ ] Database branching enabled in Vercel integration
Full guide: stacknotice.com/blog/neon-postgres-nextjs-complete-guide-2026
Top comments (0)