Building a Production SaaS with Next.js, Drizzle ORM, and PostgreSQL
After building several SaaS products, we've settled on a stack that balances developer experience with production reliability. Here's our battle-tested architecture.
The Stack
- Next.js 16 — App Router with Server Components
- Drizzle ORM — Type-safe SQL with zero overhead
- PostgreSQL 16 — The workhorse relational database
- Tailwind CSS v4 — Rapid UI development
- next-intl — Internationalization
Why Drizzle Over Prisma?
We switched from Prisma to Drizzle for several reasons:
- No runtime engine — Drizzle generates plain SQL, no binary dependency
- SQL-like API — If you know SQL, you know Drizzle
- Bundle size — Drizzle adds ~50KB vs Prisma's ~2MB runtime
- Type inference — Schema-first approach with excellent TypeScript inference
const posts = await db.query.blogPosts.findMany({
where: eq(blogPosts.published, true),
with: { category: true },
orderBy: [desc(blogPosts.publishedAt)],
limit: 10,
});
Architecture Patterns
Data Access Layer
We organize database queries into a clean data access layer:
src/
lib/
db/
schema.ts # Drizzle schema definitions
index.ts # DB connection
queries/
posts.ts # Post-related queries
users.ts # User-related queries
Server Actions
For mutations, we use Server Actions instead of API routes:
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.insert(blogPosts).values({ title });
revalidatePath('/blog');
}
Deployment
Our production deployment:
- PM2 for process management
- PostgreSQL 16 on the same server
- Apache2 reverse proxy
- Let's Encrypt for SSL
This stack serves thousands of requests per second on a single €30/month VPS.
Originally published on IceCat Studio Blog. Based on production experience across multiple SaaS projects.