Back to Knowledge
Updated Sep 2026
GuideEssential

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."

Postgres to your Next.js app with Drizzle ORM

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."

. Type-safe queries, migrations

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."

, and real data in about 20 minutes.

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."

query the database directly. No API layer needed for reads.

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
1

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 neon

Route B: Neon Console

  1. 1. Sign in at console.neon.tech
  2. 2. Create a project and pick the region closest to your Vercel functions (Vercel's default region is iad1, US East)
  3. 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=require

Keep 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."

.

2

Install Dependencies

Install Drizzle, the Neon serverless driver, and dotenv (so Drizzle Kit can read .env.local):

Terminal

npm install drizzle-orm @neondatabase/serverless dotenv

And Drizzle Kit for migrations (dev dependency):

Terminal

npm install -D drizzle-kit
3

Set 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.

4

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.)

5

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)."

IS your TypeScript types. No codegen step, no drift. Change a column and TypeScript flags every broken query. The schema design guide covers relationships and indexes.

6

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.

7

Push Schema to Database

For a brand-new dev database, push the schema straight in:

Terminal

npx drizzle-kit push

You 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 browser
8

Query 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.

9

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.

10

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 --prod

Database Connected!

You're fetching real data from Postgres with full TypeScript safety. Just SQL, no vendor SDK lock-in.

Final File Structure

my-app/
.env.local← DATABASE_URL
drizzle.config.ts
drizzle/← Generated migrations
lib/
db.ts← Connection
schema.ts← Table definitions
app/
page.tsx← Fetches posts
api/posts/route.ts← POST endpoint

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));