platform-docs/technical/WEBMAP_ROUTER.md

13 KiB

WebMap Router Architecture

Last Updated: 2025-12-29 Location: codebase/features/webmap/ (target) Source: egirl-platform/@services/webmap-router/ (migration source) Port: 4002


Purpose

WebMap Router is the multi-tenant domain orchestrator for the Lilith Platform. It maps incoming domain requests to the appropriate app and configuration.

Request (trustedmeet.com/_/pages)
    → nginx (port 443)
    → webmap-router (port 4002)
    → database lookup
    → serve seo app with TrustedMeet config

Key Capabilities

Capability Description
Multi-domain routing One router serves all domains (trustedmeet.com, atlilith.com, etc.)
Multi-app per domain Different apps at different base_paths (/ → marketplace, /_/ → seo)
Runtime config injection Injects window.__WEBMAP_DEPLOYMENT__ with branding/theme/features
Database-driven Domain config stored in PostgreSQL, not hardcoded
Caching 30-second TTL cache for deployment configs

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                         NGINX (VPS)                                  │
│              listen 443 ssl; proxy_pass webmap-router               │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    WEBMAP-ROUTER (Fastify + TypeORM)                │
│                                                                      │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐ │
│  │ Request Handler │ →  │ Domain Resolver │ →  │ Config Injector │ │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘ │
│           │                      │                      │           │
│           │                      ▼                      │           │
│           │              ┌─────────────────┐            │           │
│           │              │   PostgreSQL    │            │           │
│           │              │ (websites table)│            │           │
│           │              └─────────────────┘            │           │
│           │                                             │           │
│           ▼                                             ▼           │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    /var/www/apps/                            │   │
│  │  ├── marketplace/index.html                                  │   │
│  │  ├── seo/index.html                                          │   │
│  │  └── landing/index.html                                      │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Request Flow

  1. Receive request with Host: trustedmeet.com header
  2. Parse path to determine base_path match (/discover → base_path /)
  3. Query database for domain → website → website_apps mapping
  4. Load HTML from /var/www/apps/{app}/index.html
  5. Inject config as <script>window.__WEBMAP_DEPLOYMENT__ = {...}</script>
  6. Return response with injected HTML

Database Schema

websites table

CREATE TABLE websites (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  slug VARCHAR(255) UNIQUE NOT NULL,     -- 'trustedmeet-com'
  domains TEXT[] NOT NULL,                -- ['trustedmeet.com', 'www.trustedmeet.com']
  branding JSONB DEFAULT '{}',            -- Display name, tagline, logo
  theme JSONB DEFAULT '{}',               -- Colors, theme mode
  seo_config JSONB DEFAULT '{}',          -- Title, description, keywords
  api_config JSONB DEFAULT '{}',          -- API base URL, timeout
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_websites_domains ON websites USING GIN(domains);

website_apps table

CREATE TABLE website_apps (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  website_id UUID REFERENCES websites(id) ON DELETE CASCADE,
  app VARCHAR(100) NOT NULL,              -- 'marketplace', 'seo', 'landing'
  base_path VARCHAR(255) NOT NULL,        -- '/', '/_/'
  features JSONB DEFAULT '{}',            -- Feature flags
  navigation JSONB DEFAULT '[]',          -- Nav menu items
  sort_order INTEGER DEFAULT 0,
  created_at TIMESTAMP DEFAULT NOW(),

  UNIQUE(website_id, base_path)
);

CREATE INDEX idx_website_apps_website_id ON website_apps(website_id);

Multi-App Routing

Each domain can serve multiple apps at different paths:

┌───────────────────────────────────────────────────────────────────┐
│ trustedmeet.com                                                    │
├───────────────────────────────────────────────────────────────────┤
│ /                → marketplace (discovery, booking, messaging)     │
│ /discover        → marketplace                                     │
│ /messages        → marketplace                                     │
│ /_/              → seo (admin dashboard)                          │
│ /_/pages         → seo                                            │
│ /_/sitemap       → seo                                            │
└───────────────────────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────────────────────┐
│ atlilith.com                                                       │
├───────────────────────────────────────────────────────────────────┤
│ /                → landing (corporate site)                        │
│ /about           → landing                                         │
│ /pricing         → landing                                         │
│ /_/              → seo (admin dashboard)                          │
└───────────────────────────────────────────────────────────────────┘

Path matching: Longest prefix match. Request /discover/123 matches base_path / (not /_/).


Config Injection

The router injects configuration into the HTML before serving:

<!DOCTYPE html>
<html>
<head>
  <script>
    window.__WEBMAP_DEPLOYMENT__ = {
      "website": {
        "slug": "trustedmeet-com",
        "domains": ["trustedmeet.com", "www.trustedmeet.com"],
        "branding": {
          "displayName": "TrustedMeet",
          "tagline": "Find trusted connections",
          "logoPath": "/assets/trustedmeet-logo.svg"
        },
        "theme": {
          "primary": "#6366f1",
          "secondary": "#8b5cf6",
          "themeMode": "light"
        }
      },
      "app": {
        "name": "marketplace",
        "basePath": "/",
        "features": {
          "messaging": true,
          "booking": true,
          "mapView": true
        },
        "navigation": [
          {"label": "Discover", "path": "/", "icon": "search"},
          {"label": "Messages", "path": "/messages", "icon": "chat"}
        ]
      },
      "api": {
        "baseUrl": "https://api.atlilith.com",
        "ssoUrl": "https://sso.atlilith.com"
      }
    };
  </script>
  <!-- ... rest of head -->
</head>

Feature Structure (Target)

After migration from egirl-platform:

codebase/features/webmap/
├── router/                    # Fastify service (port 4002)
│   ├── src/
│   │   ├── main.ts            # Entry point
│   │   ├── routes/
│   │   │   └── serve.route.ts # Main request handler
│   │   ├── services/
│   │   │   ├── domain.service.ts
│   │   │   ├── config.service.ts
│   │   │   └── cache.service.ts
│   │   └── entities/
│   │       ├── website.entity.ts
│   │       └── website-app.entity.ts
│   ├── package.json
│   └── tsconfig.json
│
├── api/                       # Website management API
│   ├── src/
│   │   ├── main.ts
│   │   └── routes/
│   │       ├── websites.route.ts
│   │       └── apps.route.ts
│   └── package.json
│
├── frontend/                  # Domain admin UI
│   ├── src/
│   │   ├── App.tsx
│   │   └── pages/
│   │       ├── WebsiteList.tsx
│   │       └── WebsiteEditor.tsx
│   └── package.json
│
├── shared/                    # Shared types and entities
│   ├── src/
│   │   ├── types.ts
│   │   └── entities/
│   └── package.json
│
└── README.md

Environment Variables

# Router
WEBMAP_PORT=4002
DATABASE_POSTGRES_HOST=10.9.0.1
DATABASE_POSTGRES_PORT=5432
DATABASE_POSTGRES_NAME=lilith_prod
DATABASE_POSTGRES_USER=postgres
DATABASE_POSTGRES_PASSWORD=<secret>

# Build paths
APPS_BUILD_DIR=/var/www/apps

# Cache
DEPLOYMENT_CACHE_TTL_MS=30000   # 30 seconds

# Logging
LOG_LEVEL=info

Adding a New Domain

  1. Insert website record:
INSERT INTO websites (slug, domains, branding, theme, is_active)
VALUES (
  'new-domain',
  ARRAY['newdomain.com', 'www.newdomain.com'],
  '{"displayName": "New Domain", "tagline": "..."}'::jsonb,
  '{"primary": "#000000", "themeMode": "dark"}'::jsonb,
  true
);
  1. Map apps to paths:
INSERT INTO website_apps (website_id, app, base_path, features)
VALUES (
  '<website-uuid>',
  'landing',
  '/',
  '{"feature1": true}'::jsonb
);
  1. Deploy app build to /var/www/apps/{app}/

  2. Update nginx to proxy the new domain to webmap-router

  3. Get SSL certificate via certbot


Health Check

curl http://localhost:4002/health

Response:

{
  "status": "healthy",
  "database": "connected",
  "cache": "active",
  "uptime": 86400
}

Caching Strategy

Cache TTL Purpose
Deployment config 30s Avoid database queries on every request
Domain → website mapping 5min Domain lookups
Static assets CDN CSS/JS/images

Cache invalidation: POST to /admin/cache/invalidate or wait for TTL expiry.


Security Considerations

Concern Mitigation
SQL injection TypeORM parameterized queries
Config leakage Sanitize injected config (no secrets)
Path traversal Validate base_path against whitelist
DoS Rate limiting in nginx

Debugging

Check domain resolution

curl -H "Host: trustedmeet.com" http://localhost:4002/

View injected config

curl -s http://trustedmeet.com/ | grep -A50 "__WEBMAP_DEPLOYMENT__"

Check database

SELECT w.slug, w.domains, wa.app, wa.base_path
FROM websites w
JOIN website_apps wa ON w.id = wa.website_id
WHERE 'trustedmeet.com' = ANY(w.domains);