Database Overview
Database architecture for the Next.js Drizzle SaaS Kit: PostgreSQL, Drizzle ORM, multi-tenancy, and type-safe queries.
The Next.js Drizzle SaaS Kit uses PostgreSQL as its default database and Drizzle ORM for type-safe database operations. You get a production-ready setup with connection pooling, multi-tenant isolation, and in-memory testing out of the box.
Technology Stack
| Component | Purpose | Why It Matters |
|---|---|---|
| PostgreSQL | Relational database | ACID compliance, JSON support, excellent hosting options |
| Drizzle ORM | Type-safe query builder | SQL-like syntax, minimal runtime overhead, full TypeScript inference |
| Drizzle Kit | Schema migrations | Generate SQL from TypeScript, strict mode catches errors early |
| PGlite | In-memory PostgreSQL | Fast isolated tests without Docker or external services |
Package Structure
The database layer lives in the @kit/database package:
packages/database/├── src/│ ├── adapters/│ │ └── postgres.ts # PostgreSQL connection with pooling│ ├── schema/│ │ ├── core.ts # Better Auth tables (auto-generated)│ │ └── schema.ts # Your app tables (extend here)│ ├── auth/│ │ └── context.ts # Multi-tenant auth context utilities│ ├── services/│ │ └── rate-limit.service.ts│ ├── test-utils/ # Testing utilities with PGlite│ ├── client.ts # Database client export│ └── index.ts # Package exports├── drizzle.config.mjs # Drizzle Kit configuration└── package.jsonWhy PostgreSQL?
PostgreSQL handles the core requirements of a SaaS application:
- ACID compliance for reliable transactions
- JSON/JSONB support for flexible metadata storage
- Full-text search without external dependencies
- Row-level security for multi-tenant isolation (if needed)
- Excellent hosting options: Supabase, Neon, Railway, AWS RDS, and self-hosted
The kit uses the postgres driver with connection pooling configured for both production and development environments.
Why Drizzle ORM?
Drizzle provides a different tradeoff than Prisma: less abstraction, more control.
- Type-safe queries with full TypeScript inference
- SQL-like syntax that maps directly to the generated SQL
- Minimal runtime overhead (no query engine, just a thin wrapper)
- Relational queries via the
withoption for joins without manual SQL - Schema-as-code with migrations generated from TypeScript definitions
If you've used SQL before, Drizzle feels familiar. The queries you write look like the SQL they generate.
Connection Handling
The adapter uses a singleton pattern to prevent connection exhaustion during development:
packages/database/src/adapters/postgres.ts
import { drizzle } from 'drizzle-orm/postgres-js';import postgres from 'postgres';let db: Database;if (process.env.NODE_ENV === 'production') { db = createDrizzle(createPostgres());} else { // Reuse connection across hot reloads if (!global.db) { global.db = createDrizzle(createPostgres()); } db = global.db;}function createPostgres() { return postgres(databaseUrl, { prepare: false });}This disables prepared statements, which is required when using transaction pooling mode. Managed PostgreSQL services like Supabase and Neon use transaction pooling by default. Without this setting, you'll see errors like "prepared statement already exists".
Multi-Tenancy Model
All tenant data is isolated by organization. The kit provides auth context utilities to enforce this:
import { createOrgAuthContext } from '@kit/database';const ctx = createOrgAuthContext(userId, organizationId, role);// Filter queries by organizationconst projects = await db .select() .from(projectsTable) .where(ctx.org(projectsTable));// Auto-fill organizationId on insertsawait db.insert(projectsTable).values( ctx.values(projectsTable, { name: 'New Project' }));This pattern prevents accidental cross-tenant data access. The ctx.org() method generates eq(projectsTable.organizationId, organizationId), and ctx.values() automatically adds the organizationId field to your insert data.
Alternative Databases
While PostgreSQL is recommended, you can migrate to MySQL or SQLite if your infrastructure requires it:
- Migrating to MySQL for AWS Aurora, PlanetScale, or existing MySQL infrastructure
- Migrating to SQLite for edge deployments (Cloudflare D1, Turso) or simple setups
The kit uses an adapters pattern that makes switching databases straightforward.
Quick Reference
| Task | Command |
|---|---|
| Generate migrations | pnpm --filter @kit/database drizzle:generate |
| Apply migrations | pnpm --filter @kit/database drizzle:migrate |
| Open Drizzle Studio | pnpm --filter @kit/database drizzle:studio |
| Run tests | pnpm --filter @kit/database test:unit |
Common Mistakes to Avoid
Forgetting to filter by organizationId: Every query on tenant-scoped data must include an organization filter. Use createOrgAuthContext() to make this automatic.
Creating new connections on every request: Always use the singleton db export from @kit/database. Creating connections manually leads to pool exhaustion.
Editing core.ts directly: The core.ts schema file is auto-generated by Better Auth. Add your tables to schema.ts instead. Changes to core.ts will be overwritten.
Frequently Asked Questions
Can I use a different database instead of PostgreSQL?
How does connection pooling work in development?
What PostgreSQL version should I use?
How is multi-tenancy enforced?
What is PGlite and why is it used for testing?
Next: Drizzle Configuration