Multi-Tenant Dashboard Architecture

Type: Execution Recipe Confidence: 0.88 Sources: 6 Verified: 2026-03-13

Purpose

This recipe produces a multi-tenant startup dashboard platform where each tenant (company/startup) sees only their own data, with per-tenant branding, customizable KPI layouts, and isolated user management. The output is a deployed SaaS application using PostgreSQL Row Level Security for data isolation, automated tenant provisioning, and a super-admin panel for managing tenants. [src1]

Prerequisites

Constraints

Tool Selection Decision

Which isolation model?
├── Budget-optimized AND tenants < 200
│   └── PATH A: Shared Schema + RLS — one database, tenant_id on every row
├── Moderate isolation AND tenants < 50
│   └── PATH B: Schema-per-Tenant — one database, separate schema per tenant
├── Maximum isolation AND enterprise customers
│   └── PATH C: Database-per-Tenant — dedicated database per customer
└── Hybrid (most common for growth-stage SaaS)
    └── PATH D: Tiered — RLS for free/basic, dedicated schema for paid, dedicated DB for enterprise
PathIsolationCostComplexityBest For
A: Shared + RLSLogical (row-level)LowestLowStartups, < 200 tenants
B: Schema-per-TenantSchema-levelMediumMediumMid-market, 10-50 tenants
C: Database-per-TenantFull physicalHighestHighEnterprise, compliance-heavy
D: Tiered HybridMixedVariesHighGrowth-stage SaaS at scale

Execution Flow

Step 1: Design the Multi-Tenant Data Model

Duration: 1-2 hours · Tool: SQL client

Create the 4-table schema: tenants (with branding and settings JSONB), tenant_users (with auth provider linkage and roles), tenant_metrics (with tenant-scoped KPI storage), and tenant_dashboards (with per-tenant widget layout JSONB).

Verify: All 4 tables created with indexes. If failed: Check PostgreSQL version — RLS requires 9.5+, gen_random_uuid() requires pgcrypto or pg13+.

Step 2: Implement Row Level Security

Duration: 1-2 hours · Tool: SQL client

Enable RLS on all tenant-scoped tables. Create USING and WITH CHECK policies that read the tenant context from a session variable (app.current_tenant_id). Application middleware sets this variable on every request from the authenticated user's JWT claims.

Verify: Insert test data for two tenants. Query as Tenant A — Tenant B data is invisible. Attempt cross-tenant INSERT — verify it fails. If failed: Run SELECT tablename, rowsecurity FROM pg_tables; to confirm RLS is enabled.

Step 3: Build Tenant Provisioning

Duration: 2-3 hours · Tool: Node.js / API endpoint

Create a provisioning endpoint that: creates tenant record with branding defaults, creates owner user linked to auth provider, generates default dashboard layout, seeds sample metrics for onboarding, and sends welcome email.

Verify: Log in as new tenant owner — dashboard loads with sample data, branding defaults applied. If failed: Check auth provider user creation and tenant_id propagation.

Step 4: Build Per-Tenant Customization

Duration: 2-3 hours · Tool: Next.js / React

Build branding middleware that loads tenant config from DB with Redis caching (5-min TTL). Create dashboard layout engine that renders widgets from tenant-specific JSONB configuration. Build tenant settings page for branding, widget selection, and data source connections.

Verify: Change tenant branding — logo and colors update on next load. Add/remove widgets — layout persists. If failed: Check Redis cache invalidation on branding updates.

Step 5: Build Tenant Routing and Authentication

Duration: 2-3 hours · Tool: Next.js middleware

Implement subdomain routing (acme.dashboard.io) or path-based routing (/app/acme/) using Next.js middleware. Verify authenticated user belongs to the resolved tenant before granting access. Return 403 for cross-tenant access attempts.

Verify: Access two different tenant subdomains — each shows different branding and data. Cross-tenant access returns 403. If failed: Check DNS wildcard and SSL certificate configuration.

Step 6: Deploy and Monitor

Duration: 1-2 hours · Tool: Vercel or Cloudflare Pages

Configure wildcard DNS, set environment variables, create super-admin panel for tenant management, set up per-tenant monitoring (query latency, error rates, storage usage), and configure provisioning failure alerts.

Verify: Provision a new tenant via admin panel — full onboarding flow works end-to-end.

Output Schema

{
  "output_type": "multi_tenant_dashboard_platform",
  "format": "deployed SaaS application",
  "components": [
    {"name": "tenant_provisioning", "type": "api", "description": "REST endpoint for automated tenant creation", "required": true},
    {"name": "rls_isolation", "type": "database", "description": "PostgreSQL RLS policies for zero cross-tenant data access", "required": true},
    {"name": "branding_engine", "type": "config", "description": "Per-tenant logo, colors, and favicon with cached resolution", "required": true},
    {"name": "dashboard_customizer", "type": "ui", "description": "Widget layout with per-tenant persistence", "required": true},
    {"name": "subdomain_routing", "type": "middleware", "description": "Wildcard subdomain resolution to tenant context", "required": true},
    {"name": "admin_panel", "type": "ui", "description": "Super-admin view for tenant management and monitoring", "required": true}
  ],
  "tenant_capacity": "1-200 tenants on shared schema",
  "data_source": "PostgreSQL with Row Level Security"
}

Quality Benchmarks

Quality MetricMinimum AcceptableGoodExcellent
Data isolationRLS on all tenant tables+ integration tests per release+ automated penetration testing
Provisioning time< 60 seconds< 15 seconds< 5 seconds (fully automated)
Query latency (p95)< 2 seconds< 500ms< 200ms
Customization scopeBranding only+ widget layout+ custom data sources + formulas
Tenant onboardingManual admin setupSelf-service signup+ Stripe billing + auto-provisioning

If below minimum: Check RLS policy coverage — every table with tenant_id MUST have RLS enabled. Query latency above 2s usually means missing indexes on tenant_id columns.

Error Handling

ErrorLikely CauseRecovery Action
Cross-tenant data leakRLS not enabled on table or missing policyAudit all tables: SELECT tablename, rowsecurity FROM pg_tables;
Tenant provisioning timeoutAuth provider rate limit or DB pool exhaustionImplement retry with backoff; increase connection pool
Subdomain not resolvingDNS wildcard not configured or SSL missingAdd *.domain CNAME record; request wildcard SSL
RLS blocks all rows (empty results)Session variable not set by middlewareAdd error handling: return 401 if no tenant context
Noisy neighbor degrading all tenantsOne tenant running expensive aggregationAdd per-tenant SET statement_timeout; implement query cost limits
Branding not updatingRedis cache not invalidatedInvalidate cache key on branding update

Cost Breakdown

ComponentFree (< 5 tenants)Growth (5-50)Scale (50-200)
Database (Supabase)$0 (500MB)$25/mo$75/mo
Hosting (Vercel)$0$20/mo$20/mo
Auth (Clerk)$0 (10K MAU)$25/mo$100/mo
Redis (Upstash)$0 (10K cmds/day)$10/mo$30/mo
Total$0$80/mo$225/mo

Anti-Patterns

Wrong: Using application-level filtering instead of RLS

Relying on WHERE tenant_id = ? in every query means one missed clause leaks all tenant data. Application bugs or raw SQL without the filter create immediate cross-tenant exposure. [src1]

Correct: Enforce isolation at the database level with RLS

PostgreSQL RLS policies apply automatically to every query, regardless of how it's constructed. Even if application code forgets the tenant filter, the database enforces isolation. [src6]

Wrong: Forking codebase per tenant for customization

Starting with "just a few custom features for our biggest customer" turns into unmaintainable code branches. By tenant 5, every bug fix requires patching N branches. [src4]

Correct: Build a configuration-driven customization engine

Store all per-tenant differences as config (branding JSON, widget layouts, feature flags). One codebase, one deployment, N configurations.

When This Matters

Use when building a dashboard platform that will serve multiple startups or companies from a single codebase. Critical when moving from "internal tool" to "SaaS product" — the inflection point is typically 3+ external users requesting access. This recipe builds the multi-tenant infrastructure layer — for actual dashboard content, use domain-specific dashboard recipes.

Related Units