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.