If you're building a serverless app in 2026 and you haven't tried Drizzle ORM with Neon Postgres, you're missing out on one of the cleanest database setups available. No heavy abstractions, full TypeScript safety, and a serverless database that doesn't charge you when nobody's using it.
I've been running this stack in production for RAXXO Studio for months. Here's everything I wish someone had told me upfront.
Why Drizzle Over Prisma?
Prisma is fine. It works. But Drizzle gives you something Prisma doesn't: SQL-level control with TypeScript safety. Your queries look like SQL, which means when you need to optimize something, you're not fighting an abstraction layer.
Drizzle schemas are just TypeScript objects. No separate schema file, no code generation step, no binary engine that needs to match your deployment platform. Define your tables, import them, query them.
import { pgTable, text, integer, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: text('id').primaryKey(),
clerkId: text('clerk_id').unique().notNull(),
email: text('email').unique().notNull(),
plan: text('plan').default('free').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
Setting Up Neon
Neon gives you a Postgres database that scales to zero. When nobody's querying it, you pay nothing. When traffic spikes, it handles it. For a solo developer, this is the dream - you're not paying EUR 15/month for a database that sits idle 23 hours a day.
Create a project on neon.tech, grab the connection string, and add it to your environment:
POSTGRES_URL=postgresql://user:password@ep-something.us-east-2.aws.neon.tech/neondb?sslmode=require
The Connection Pattern That Matters
In serverless environments (Vercel, Cloudflare Workers), database connections are tricky. Each function invocation might create a new connection. Neon handles this with their serverless driver, but you still want to be smart about it.
I use a lazy Proxy pattern that only creates the connection when it's actually needed:
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
let db: ReturnType<typeof drizzle>;
export function getDb() {
if (!db) {
const sql = neon(process.env.POSTGRES_URL!);
db = drizzle(sql);
}
return db;
}
This avoids connecting at build time (which would fail on Vercel) while still reusing the connection within a single function execution.
Migrations Without the Pain
Drizzle Kit handles migrations. Define your schema changes in TypeScript, generate SQL migrations, push them to Neon:
npx drizzle-kit generate
npx drizzle-kit push
For prototyping, drizzle-kit push applies schema changes directly without creating migration files. For production, use generate to create versioned SQL files you can review before applying.
Querying Patterns That Work
Drizzle's query API is where it shines. It feels like writing SQL but with full autocompletion:
// Simple select
const user = await db.select()
.from(users)
.where(eq(users.clerkId, clerkId))
.limit(1);
// Join with conditions
const profiles = await db.select()
.from(brandProfiles)
.where(eq(brandProfiles.userId, userId))
.orderBy(desc(brandProfiles.createdAt));
// Upsert pattern
await db.insert(usage)
.values({ userId, analyses: 1, periodStart: new Date() })
.onConflictDoUpdate({
target: usage.userId,
set: { analyses: sql`${usage.analyses} + 1` }
});
Indexing for Performance
Don't skip indexes. Even on a small database, the right indexes turn a 200ms query into a 2ms query. Add them in your schema:
export const usersEmailIdx = index('users_email_idx').on(users.email);
export const profilesUserIdx = index('brand_profiles_user_id_idx').on(brandProfiles.userId);
Run drizzle-kit push and they're live. Check Neon's query insights to find slow queries that need indexing.
Common Gotchas
Connection pooling: Neon's serverless driver uses HTTP, not persistent connections. This is actually what you want in serverless - no connection pool to manage or leak.
Cold starts: Neon databases scale to zero after 5 minutes of inactivity. The first query after idle takes about 500ms extra. For most apps this is fine. If it bothers you, set a minimum compute size.
Transactions: HTTP-mode Neon doesn't support multi-statement transactions. If you need them, use the WebSocket driver instead of the HTTP one.
Type safety: Drizzle infers types from your schema. Don't manually type your query results - let TypeScript infer them. If you need to export a type, use typeof users.$inferSelect.
Production Tips
Run your Neon database in the same region as your Vercel deployment. I use US East (Virginia) for both, keeping latency under 10ms for most queries.
Use Neon's branching feature for staging. Create a branch of your production database, test your migration against it, then merge. It's like git branches but for your database.
Monitor your usage on Neon's dashboard. The free tier gives you plenty for a solo project, but keep an eye on compute hours if you're running background jobs.
RAXXO Studio runs entirely on Drizzle + Neon, handling user accounts, usage tracking, brand profiles, and generation history. Try it at studio.raxxo.shop.