Building a SaaS with Next.js and ZenStack
How schema-driven development with ZenStack eliminates boilerplate and keeps your data layer type-safe from database to UI.
By Alex Malahov
Starting a new SaaS project means making dozens of decisions upfront: auth, database access, API design, access control. Most of these decisions end up producing boilerplate that drifts out of sync over time.
The Schema-First Approach
ZenStack takes a different approach. You define your data model once in a .zmodel file — including access policies — and it generates everything downstream:
- Prisma schema and migrations
- TypeScript types
- TanStack Query hooks for the frontend
- Runtime access control enforcement
model Project {
id String @id @default(cuid())
name String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
@@allow('read', auth() != null && organization.members?[userId == auth().id])
@@allow('create', auth() != null && organization.members?[userId == auth().id && role == ADMIN])
}Why This Matters
When your access policies live next to your data model, they cannot drift. The compiler enforces consistency. Your API layer becomes a thin pass-through because the data layer already knows who can do what.
What's Next
In upcoming posts, I'll cover how we handle multi-tenancy, how the auth flow works end to end, and how to test ZenStack policies effectively.