malahov.io

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.

1 min read

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
typescript
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.