Skip to content

Our own product · Vertical SaaS builds

Eight products, one permission model

Zeno is eight applications — CRM, Work, Desk, Docs, HR, Campaigns, POS and Dev — sharing one API, one Postgres database and one design system. The interesting engineering is in what they share, not in any one of them.

Context

Zeno is our business platform for small and mid-sized teams — the kind that have outgrown a spreadsheet and do not want a six-month configuration project. Zeno CRM is the module deployed and running today; the others sit at varying stages behind it.

Eight applications is the part that sounds impressive and is actually the liability. Anyone can build eight apps. The question is whether they are eight products or one, and that question is settled almost entirely by what they share underneath.

What was hard

Access control multiplies. Eight apps times several roles times row-level tenancy is the point where most suites quietly diverge: each app grows its own permission checks, they disagree at the edges, and the disagreement is discovered by a customer seeing a record they should not.

Consistency is not a style guide. A user moving from Work to CRM should never have to relearn where things are. Documenting that in a design file does not achieve it — the second app drifts within a sprint. It has to be structural.

One database, many products, no cross-contamination. Sharing one Postgres instance across eight applications is what makes the suite coherent — a customer record is the same record everywhere — and is also the thing most likely to produce a leak between tenants.

What we built

One NestJS API, one Supabase Postgres, one design system, eight Next.js applications. Permissions live in a shared package with an explicit checker and role model rather than being re-implemented per app, so a permission question has exactly one answer and one place to fix it.

The schema has moved through 319 migrations, append-only. Tenancy is enforced at the row level in the database, which means a mistake in application code surfaces as a query returning nothing rather than as a query returning somebody else's data.

The Work application is the reference implementation of the design standard. When the written design documentation and the live tokens disagree — and they did — the live tokens win and the documentation is corrected. Design authority lives in code that ships, not in a document that drifts.

Technical decisions and why

Row-level security over application-layer filtering. Application-layer filtering is easier to write and easier to forget. Enforcing tenancy in the database means the failure mode of a missed check is an empty result rather than a disclosure. It costs more up front, in policy authoring and in tests that assert the negative case, and it removes an entire category of incident.

A shared permissions package, not a shared permissions convention. A convention holds until someone is in a hurry. A package with a single checker means the eighth app cannot invent its own interpretation of what "admin" means.

Append-only migrations, without exception. 319 migrations is not elegant. It is, however, a complete and replayable history of how the schema reached its current state, which is worth considerably more than elegance the first time a production restore is needed.

One monorepo, one toolchain. Turborepo with pnpm workspaces, shared ESLint, TypeScript and Tailwind configuration. The alternative — eight repositories with eight subtly different build setups — is how a suite becomes eight products with a shared logo.

Outcome

Zeno CRM runs in production and we carry its migrations, its uptime and its support load. The shared foundation is doing the job it was built for: adding an application means building the application, not rebuilding authentication, permissions, tenancy and chrome.

No adoption or performance figures are published here. They exist, they are ours, and they say nothing useful about what your suite would do.

Stack

  • Runtime — TypeScript throughout; Next.js applications against a NestJS API
  • Data — Supabase Postgres with row-level security; 319 append-only migrations
  • Shared packages — permissions, auth, billing, notifications, types, UI, analytics
  • Tooling — Turborepo, pnpm workspaces, shared ESLint/TypeScript/Tailwind config
  • Testing — Playwright end-to-end, Lighthouse CI, SonarQube