Compliance platforms, B2B dashboards, and AI tools often serve many organisations (exporters, cooperatives, brands) from one codebase. The frontend has to feel tailored to each tenant without becoming a different app for each.
1. Resolve the tenant once
Options:
| Strategy | Example | Notes |
|---|---|---|
| Subdomain | acme.app.com | Branded, good cookie isolation |
| Path | app.com/acme | Simplest |
| Custom domain | compliance.acme.com | Most work: routing + TLS |
Resolve the tenant in middleware or the root layout, then pass a tenant context down. Every data request includes the tenant, and the backend verifies the user belongs to it.
2. Theme with tokens
:root { --brand: 24 95% 53%; --brand-foreground: 0 0% 100%; }
<html style={{ "--brand": tenant.theme.brand } as React.CSSProperties}>
Components use bg-[hsl(var(--brand))] or a Tailwind theme mapped to variables, never hard-coded colours. This builds on a token-based design system.
3. Role-based UI
const can = (user: User, action: Action) => user.permissions.includes(action); {can(user, "plots:approve") && <ApproveButton />}
- Centralise permission checks in one helper.
- Hide what users can't do, but enforce on the server.
4. Feature flags per tenant
Tenants buy different plans or opt into betas. Use flags (tenant.features.includes("satellite-checks")) rather than branching code per customer name.
5. Isolation pitfalls
- Caching: never cache tenant-specific data under a shared key. Include the tenant ID in cache keys and tags (Next.js caching).
- Client state: clear stores when switching tenants.
- Analytics: tag events with the tenant for debugging, but respect data agreements.
6. Test the matrix
Test at least: two tenants × two roles × feature flag on/off for critical flows.