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
- Receive request with
Host: trustedmeet.comheader - Parse path to determine base_path match (
/discover→ base_path/) - Query database for domain → website → website_apps mapping
- Load HTML from
/var/www/apps/{app}/index.html - Inject config as
<script>window.__WEBMAP_DEPLOYMENT__ = {...}</script> - 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
- 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
);
- Map apps to paths:
INSERT INTO website_apps (website_id, app, base_path, features)
VALUES (
'<website-uuid>',
'landing',
'/',
'{"feature1": true}'::jsonb
);
-
Deploy app build to
/var/www/apps/{app}/ -
Update nginx to proxy the new domain to webmap-router
-
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);
Related Documentation
- MVP_LAUNCH_V1.md - V1 launch overview
- TRUSTEDMEET_DEPLOYMENT.md - Deployment steps
- SEO Feature README - SEO app docs