Nexus SaaS
All-in-one multi-tenant business management platform for teams who need more than spreadsheets.
The Challenge
The core challenge was implementing true multi-tenancy without the cost and complexity of a database-per-tenant model. Instead, I designed a shared-database, shared-schema architecture with row-level tenant isolation enforced at the query layer via Prisma. Every tenant-scoped model carries a tenantId foreign key, and all server actions resolve the current tenant before any data operation, making cross-tenant data leakage structurally impossible. The second major challenge was the authorization model. Building a 50+ permission RBAC system with custom role definitions, per-tenant feature flags, and middleware-enforced route protection required careful schema design and a centralized permission-key convention using module.entity.action dot notation. Webhook reliability was the third tricky area. Rather than relying on idempotency at the Stripe level, I introduced a WebhookEvent deduplication table that records every processed event ID before any business logic runs, turning an at-least-once delivery guarantee into an exactly-once processing guarantee.
Architectural Decisions
Shared-schema multi-tenancy with Prisma row-level isolation
Chose shared database over database-per-tenant to avoid infrastructure overhead and migration complexity. All 20+ tenant-scoped models carry a tenantId foreign key, and a centralized tenant resolution utility injects the tenant context into every query.
Next.js Server Actions as the primary data mutation layer
Instead of building a REST or tRPC API layer, all client-to-server mutations use Next.js 16 Server Actions. Each action calls requireTenantPermission() before touching the database, making it impossible to bypass auth by hitting an unprotected endpoint.
Webhook idempotency via WebhookEvent deduplication table
Stripe webhooks deliver events at-least-once. To prevent double-processing billing events, every incoming webhook is first checked against a WebhookEvent table keyed on the Stripe event ID. Only new, unseen events are processed.
Granular RBAC with custom role definitions
Designed a three-tier authorization model: super-admin, tenant-admin, and tenant-user with RBAC via 50+ permissions in module.entity.action format. Tenants can define custom roles with allowlists of features and permissions.
Per-tenant feature flags for module gating
Each tenant has a TenantFeature join table that enables or disables modules independently. This decouples billing logic from feature access and keeps the tenant-facing UI clean by hiding disabled modules entirely.
Subdomain-based tenant routing at the middleware layer
Tenants are resolved by extracting the subdomain from the incoming request hostname inside Next.js middleware, before any React rendering occurs. The resolved tenant is passed into the request context without prop drilling.