Neon and Drizzle ORM are the most popular combination for Next.js applications that need a real Postgres database. Drizzle is TypeScript-native, generates zero-overhead queries, and integrates with Neon's HTTP driver — which is essential for Edge Runtime and Vercel Functions where the standard pg TCP driver doesn't work.
Why the HTTP Driver Matters
Standard Postgres drivers (pg, postgres.js) use TCP connections. Vercel Edge Functions and Cloudflare Workers don't support TCP — they only allow HTTP/WebSocket connections. Neon's @neondatabase/serverless package provides an HTTP-based driver that works everywhere.
| Driver | Works in Edge Runtime | Works in Node.js | Connection pooling |
|---|---|---|---|
| pg (standard) | No | Yes | Manual |
| @neondatabase/serverless (HTTP) | Yes | Yes | Built-in |
| @neondatabase/serverless (WebSocket) | Yes | Yes | Built-in |
Project Setup
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit tsx// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './lib/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
// Use direct URL for migrations
url: process.env.DATABASE_URL_DIRECT!,
},
});Schema Definition
// lib/db/schema.ts
import {
pgTable, uuid, text, timestamp,
integer, vector, boolean
} from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').unique().notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
export const documents = pgTable('documents', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
content: text('content').notNull(),
processed: boolean('processed').default(false),
createdAt: timestamp('created_at').defaultNow(),
});
export const chunks = pgTable('chunks', {
id: uuid('id').primaryKey().defaultRandom(),
documentId: uuid('document_id').references(() => documents.id, { onDelete: 'cascade' }),
chunkText: text('chunk_text').notNull(),
embedding: vector('embedding', { dimensions: 1536 }),
chunkIndex: integer('chunk_index').notNull(),
});Database Client Setup
// lib/db/index.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
// Singleton for serverless — create once per cold start
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
// Type helpers
export type Document = typeof schema.documents.$inferSelect;
export type NewDocument = typeof schema.documents.$inferInsert;Common Query Patterns
import { db } from '@/lib/db';
import { documents, chunks } from '@/lib/db/schema';
import { eq, desc, and, sql } from 'drizzle-orm';
// Select with filter
const userDocs = await db
.select()
.from(documents)
.where(eq(documents.userId, userId))
.orderBy(desc(documents.createdAt))
.limit(20);
// Insert and return
const [doc] = await db
.insert(documents)
.values({ userId, title, content })
.returning();
// Update
await db
.update(documents)
.set({ processed: true })
.where(eq(documents.id, docId));
// Vector similarity search (raw SQL for pgvector)
const similar = await db.execute(sql`
SELECT id, chunk_text, 1 - (embedding <=> ${queryEmbedding}::vector) AS similarity
FROM chunks
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT 5
`);Running Migrations
# Generate migration from schema changes
npx drizzle-kit generate
# Apply to database (uses DATABASE_URL_DIRECT)
npx drizzle-kit migrate
# For CI: apply migrations before running tests
DATABASE_URL_DIRECT=$NEON_BRANCH_URL npx drizzle-kit migrate| Metadata | Value |
|---|---|
| Title | Neon + Drizzle ORM: The Complete Setup for Next.js and Serverless Environments |
| Tool | Neon |
| Primary SEO keyword | neon drizzle orm next.js |
| Secondary keywords | neon serverless driver, drizzle neon setup, neon edge runtime, neon drizzle migration |
| Estimated read time | 9 minutes |
| Research date | 2026-04-14 |