DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Neon Postgres in Next.js 2026: The #1 Mistake That Breaks Everything

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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(),
});
Enter fullscreen mode Exit fullscreen mode

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
  },
});
Enter fullscreen mode Exit fullscreen mode

5. Add migration scripts

// package.json
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio"
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create a feature branch in git
  2. Neon auto-creates a matching database branch with a copy of your schema
  3. Your preview deployment connects to that branch's DATABASE_URL
  4. You can migrate and test without touching production data
  5. 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 transaction mode in Neon dashboard
  • [ ] Database branching enabled in Vercel integration

Full guide: stacknotice.com/blog/neon-postgres-nextjs-complete-guide-2026

Top comments (0)