Unclassified
Tenant Routing Architecture
System Overview
┌─────────────────────────────────────────────────────────────────┐
│ Browser Request │
│ saas-corp.localhost:3000/ │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Next.js Middleware │
│ (middleware.ts) │
│ ──────────────────────────────────────────────────────────── │
│ 1. Extract subdomain: "saas-corp" │
│ 2. Check if role (founder, sales) → NO │
│ 3. Check if reserved (www, app, api) → NO │
│ 4. Look up tenant slug "saas-corp" → FOUND │
│ 5. Inject headers: │
│ X-Tenant-Slug: saas-corp │
│ X-Tenant-Id: tenant-1 │
│ 6. Route to (tenant) group │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Next.js Routing │
│ Route to: app/(tenant)/layout.tsx │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ (tenant)/layout.tsx - Server Component │
│ ──────────────────────────────────────────────────────────── │
│ Wraps children with: │
│ <TenantProvider>{children}</TenantProvider> │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ TenantProvider - Client Component (Context) │
│ (lib/context/tenant-context.tsx) │
│ ──────────────────────────────────────────────────────────── │
│ 1. Read headers via headers() → X-Tenant-Slug: "saas-corp" │
│ 2. Load from DataProvider: │
│ provider.tenants.getBySlug("saas-corp") │
│ 3. Get Tenant object: │
│ { │
│ id: "tenant-1", │
│ name: "SaaS Corp", │
│ slug: "saas-corp", │
│ industry: "saas", │
│ branding: { │
│ logoUrl: "🚀", │
│ primaryColor: "#3B82F6", │
│ backgroundColor: "#F0F9FF", │
│ tagline: "Build, grow, and scale" │
│ }, │
│ landing: { ... }, │
│ status: "active" │
│ } │
│ 4. Provide via React Context │
│ useTenant() → returns Tenant object │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ (tenant)/page.tsx - Server Component │
│ ──────────────────────────────────────────────────────────── │
│ Renders TenantLandingPage (client component) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ TenantLandingPage - Client Component │
│ (components/tenant/landing-page.tsx) │
│ ──────────────────────────────────────────────────────────── │
│ const tenant = useTenant() │
│ │
│ Renders: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Navigation │ │
│ │ 🚀 SaaS Corp | [Sign In] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Hero Section │ │
│ │ "Unified SaaS Operations" │ │
│ │ "Connect your entire SaaS stack" │ │
│ │ [Get Started] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Features Grid │ │
│ │ 🔗 One-Click Integrations | ⚡ Intelligent Workflows │ │
│ │ 👥 Real-Time Collaboration | 🔒 Enterprise Security │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SaaS-Specific Section │ │
│ │ Built for SaaS │ │
│ │ 20+ Pre-built Templates | 60+ Available Integrations │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ CSS Variables Applied: │
│ --primary-color: #3B82F6 │
│ --background-color: #F0F9FF │
└─────────────────────────────┬────────────────────────────────────┘
│
▼
┌──────────────┐
│ Browser │
│ Renders │
│ Landing Page │
│ (branded) │
└──────────────┘
Component Hierarchy
Root Layout
↓
(tenant) Route Group Layout
├─ TenantProvider (Context)
│ ├─ Reads: X-Tenant-Slug header
│ ├─ Loads: Tenant from DataProvider
│ └─ Provides: useTenant() hook
│
└─ (tenant)/page.tsx (Server Component)
└─ TenantLandingPage (Client Component)
├─ Navigation
│ ├─ Logo & Name
│ └─ Auth Status + CTA
├─ Hero Section
│ ├─ Headline (from tenant.landing)
│ ├─ Subhead
│ └─ CTA Button
├─ Features Grid
├─ Industry Section
├─ CTA Section
└─ Footer
Data Flow Diagram
Request: saas-corp.localhost:3000
│
├─ Middleware extracts subdomain "saas-corp"
│ ├─ Checks reserved list: NO
│ ├─ Checks role list: NO
│ └─ Looks up tenant slug
│
├─ Tenant Found: tenant-1
│ ├─ name: "SaaS Corp"
│ ├─ industry: "saas"
│ └─ branding: { primaryColor: "#3B82F6", ... }
│
├─ Middleware injects headers
│ ├─ X-Tenant-Slug: "saas-corp"
│ └─ X-Tenant-Id: "tenant-1"
│
├─ TenantProvider reads headers
│ ├─ Calls: provider.tenants.getBySlug("saas-corp")
│ └─ Gets: Tenant object
│
├─ TenantLandingPage calls useTenant()
│ ├─ Gets: Tenant object with branding
│ └─ Applies: CSS variables
│
└─ Page renders with tenant colors/logo/copy
Routing Paths
┌─────────────────────────────────────────────────────────────┐
│ Subdomain Resolution Logic │
├─────────────────────────────────────────────────────────────┤
│ │
│ subdomain.localhost:3000 │
│ │ │
│ ├─ Is role? (founder, sales, etc.) │
│ │ └─ YES → /workbench/[role] │
│ │ └─ NO → continue │
│ │ │
│ ├─ Is reserved? (www, app, api, admin, docs) │
│ │ └─ YES → serve as-is (app → /app) │
│ │ └─ NO → continue │
│ │ │
│ ├─ Is tenant slug? │
│ │ └─ YES → look up in DataProvider │
│ │ │ ├─ FOUND → inject headers, route to (tenant) │
│ │ │ └─ NOT FOUND → 404 │
│ │ └─ NO → 404 │
│ │ │
│ └─ 404 "workspace not found" │
│ │
└─────────────────────────────────────────────────────────────┘
Examples:
founder.localhost:3000
├─ Is "founder" a role? YES
└─ Route to: /workbench/founder
saas-corp.localhost:3000
├─ Is "saas-corp" a role? NO
├─ Is "saas-corp" reserved? NO
├─ Is "saas-corp" a tenant slug? YES → found in DataProvider
└─ Route to: (tenant)/page.tsx with X-Tenant-Slug header
app.localhost:3000
├─ Is "app" a role? NO
├─ Is "app" reserved? YES
└─ Route to: /app/page.tsx (tenant picker)
unknown.localhost:3000
├─ Is "unknown" a role? NO
├─ Is "unknown" reserved? NO
├─ Is "unknown" a tenant slug? NO
└─ 404 "workspace not found"
DataProvider Architecture
DataProvider (in-memory)
│
├─ tenants: TenantRepo
│ ├─ get(id) → Tenant | null
│ ├─ getBySlug(slug) → Tenant | null
│ ├─ create(data) → Tenant
│ ├─ update(id, partial) → Tenant | null
│ ├─ listAll() → Tenant[]
│ └─ validateSlug(slug) → boolean
│
└─ Storage
└─ tenants Map
├─ tenant-1: { id, name: "SaaS Corp", slug: "saas-corp", ... }
├─ tenant-2: { id, name: "HealthTech Inc", slug: "healthtech", ... }
└─ tenant-3: { id, name: "Fintech Solutions", slug: "fintech-solutions", ... }
Branding Application
Tenant Data
│
├─ branding: {
│ ├─ logoUrl: "🚀"
│ ├─ primaryColor: "#3B82F6"
│ ├─ backgroundColor: "#F0F9FF"
│ └─ tagline: "Build, grow, and scale"
│
├─ landing: {
│ ├─ headline: "Unified SaaS Operations"
│ ├─ subhead: "Connect your entire SaaS stack"
│ ├─ featuredPluginIds: [...]
│ └─ featuredAgentIds: [...]
│
└─ Applied to TenantLandingPage
│
├─ CSS Variables
│ ├─ --primary-color: #3B82F6
│ └─ --background-color: #F0F9FF
│
├─ Navigation
│ └─ Logo: "🚀" + Name: "SaaS Corp"
│
├─ Hero Section
│ └─ Headline: "Unified SaaS Operations"
│
├─ Buttons
│ └─ style={{ background: 'var(--primary-color)' }}
│
└─ Sections
└─ Industry: "Built for SaaS"
Error Handling & Fallbacks
Tenant Lookup Failures
1. Unknown Subdomain
└─ Middleware → 404 "workspace not found"
2. Tenant Exists but Suspended
└─ Middleware → ✅ Routes to page
└─ Page → Shows "Workspace Suspended" message
3. No Branding Config
└─ Uses defaults:
├─ primaryColor: #3B82F6
├─ backgroundColor: #F0F9FF
└─ logoUrl: (none)
4. TenantProvider Load Error
└─ Shows loading skeleton
└─ Logs error to console
└─ Falls back to defaults
Performance Optimizations
Request → Response Timeline
Request (t=0ms)
↓
Middleware Routing (t=1ms)
• Extract subdomain: O(1)
• Hash lookup in reserved list: O(1)
• DataProvider.getBySlug(): O(n) → O(1) with index
↓
Route to (tenant) (t=2ms)
↓
Server Component Render (t=5ms)
• TenantProvider mounts
• headers() call (no I/O)
• DataProvider.getBySlug() (in-memory)
↓
Client Hydration (t=100ms)
• TenantLandingPage mounts
• useTenant() hook reads context
• CSS variables applied (no re-render)
↓
Page Interactive (t=300ms)
Total time to interactive: ~300ms (typical)
Scaling Considerations
Current (In-Memory)
├─ Seed: 3 tenants
├─ Lookup: O(n) → O(1) with index
└─ Memory: < 1MB
With 1000 Tenants
├─ Lookup: O(1) with hash
├─ Memory: ~10MB
└─ Cache: 99% hit rate on slug lookups
With Database (Future)
├─ Query: SELECT * FROM tenants WHERE slug = $1
├─ Cache: Redis TTL=3600s
├─ Hit rate: 99%+
└─ Lookup: <10ms (cached), <100ms (uncached)
This architecture is designed to scale from development to production with minimal changes to the routing logic.