Subdomain Routing Implementation Plan
Single-Port, Multi-Subdomain Architecture for HoffDesk
Research Date: 2026-04-24
Target: Restore monolith with proper domain separation
Assignee: Socrates (Backend)
Collaboration: Daedalus (UI review), Wadsworth (ops coordination)
Problem Statement
Currently all subdomains (family.hoffdesk.com, notes.hoffdesk.com) route to the same FastAPI application on port 8000. The dashboard router intercepts root paths, causing all subdomains to show the family dashboard regardless of intent.
Solution: Starlette Host-Based Routing
Starlette provides native Host() routing that matches requests by Host header before path routing. This allows a single ASGI app to dispatch to different sub-applications based on subdomain.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Tunnel │
│ (port 443) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Starlette Router (port 8000) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Host("family.hoffdesk.com") → family_app │ │
│ │ - Dashboard (calendar, weather, health) │ │
│ │ - Family automation endpoints │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Host("notes.hoffdesk.com") → notes_app │ │
│ │ - Content generation │ │
│ │ - Blog editing suite │ │
│ │ - Admin tools │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Host("hoffdesk.com") → blog_app │ │
│ │ - Public blog (main site) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Implementation
Phase 1: Restructure as Sub-Applications (Day 1)
File: main.py
from fastapi import FastAPI
from starlette.routing import Host, Mount
# Create separate FastAPI apps for each subdomain
family_app = FastAPI(title="Family Dashboard")
notes_app = FastAPI(title="Notes & Content")
blog_app = FastAPI(title="HoffDesk Blog")
# Configure family_app
family_app.include_router(dashboard_router)
family_app.include_router(family_router, prefix="/admin")
# Configure notes_app
notes_app.include_router(content_router, prefix="/admin")
notes_app.include_router(admin_router, prefix="/admin/blog")
notes_app.include_router(brief_router, prefix="/api/v1/content/briefs")
# Configure blog_app (public)
blog_app.include_router(blog_router, prefix="/api/blog")
blog_app.include_router(webhook_router) # Public webhooks
# Mount at root for local testing, Host routing in production
app = FastAPI()
# Development: path-based mounting
# app.mount("/family", family_app)
# app.mount("/notes", notes_app)
# app.mount("/", blog_app)
# Production: host-based routing
routes = [
Host("family.hoffdesk.com", family_app, name="family"),
Host("notes.hoffdesk.com", notes_app, name="notes"),
Host("hoffdesk.com", blog_app, name="blog"),
Host("www.hoffdesk.com", blog_app, name="blog_www"),
]
# Create host-routing app
from starlette.applications import Starlette
host_router = Starlette(routes=routes)
# Export based on environment
import os
if os.getenv("HOST_ROUTING", "false").lower() == "true":
application = host_router
else:
application = app # Legacy path-based for local dev
Phase 2: Shared Components (Day 1)
Create: shared/subdomain_middleware.py
"""Middleware to inject subdomain context into request state."""
from starlette.middleware.base import BaseHTTPMiddleware
class SubdomainMiddleware(BaseHTTPMiddleware):
"""Add subdomain context to request.state for template-aware rendering."""
async def dispatch(self, request, call_next):
host = request.headers.get("host", "").lower()
if host.startswith("family."):
request.state.subdomain = "family"
request.state.theme = "dashboard"
elif host.startswith("notes."):
request.state.subdomain = "notes"
request.state.theme = "editor"
else:
request.state.subdomain = "main"
request.state.theme = "blog"
response = await call_next(request)
return response
Update: shared/session_auth.py
The session auth middleware already works across subdomains — cookies are set on .hoffdesk.com (parent domain), so auth persists across subdomains.
Phase 3: Static Files per Subdomain (Day 2)
Current issue: Static files are mounted globally. Need subdomain-aware static serving.
Solution: Mount static files within each sub-app:
# In family_app setup
family_app.mount("/static", StaticFiles(
directory="/home/hoffmann_admin/.openclaw/shared/project-docs/dashboard/static"
), name="family_static")
# In notes_app setup
notes_app.mount("/static", StaticFiles(
directory="/home/hoffmann_admin/hoffdesk/blog/static" # Shared for now
), name="notes_static")
Phase 4: URL Generation (Day 2)
With Host routing, request.url_for() works with host names:
# In templates/code
url = request.url_for("family:dashboard_page") # family.hoffdesk.com/
url = request.url_for("notes:content_generate") # notes.hoffdesk.com/admin/generate
url = request.url_for("blog:blog_list") # hoffdesk.com/api/blog/
Migration Plan
| Step | Action | Risk | Duration |
|---|---|---|---|
| 1 | Create sub-applications in new main_v2.py |
Low | 2 hrs |
| 2 | Test locally with HOST_ROUTING=true |
Low | 1 hr |
| 3 | Update systemd service to use new entry point | Medium | 30 min |
| 4 | Verify Cloudflare tunnel routes correctly | Low | 30 min |
| 5 | Update documentation & notify Daedalus | None | 30 min |
| 6 | Archive old main.py → main_legacy.py |
None | 10 min |
Daedalus Coordination Points
- Static assets: Confirm paths for dashboard vs blog static files
- HTMX base URLs: May need subdomain-aware base URL in templates
- Design tokens: Verify
design-tokens/paths work across subdomains - Testing: Test cross-subdomain navigation once live
Wadsworth Coordination Points
- Cloudflare Tunnel: Verify DNS records for
notes.hoffdesk.compoint to same tunnel - Certificate: Ensure SSL covers
*.hoffdesk.comwildcard - Monitoring: Add health checks for each subdomain endpoint
- Rollback: Keep
main_legacy.pyready for instant revert
Acceptance Criteria
- [ ]
family.hoffdesk.com/→ Family dashboard (authenticated) - [ ]
notes.hoffdesk.com/admin/blog/→ Blog admin suite (authenticated) - [ ]
notes.hoffdesk.com/admin/content/→ Content generation (authenticated) - [ ]
hoffdesk.com/api/blog/→ Public blog (no auth required) - [ ] Auth session persists across subdomain navigation
- [ ] Static assets load correctly per subdomain
- [ ] No 404s or routing conflicts
Notes
- Starlette's
Host()strips the port from matching — port 3600 in config won't block other ports - FastAPI's
root_pathhandling should work automatically for sub-apps - Consider custom middleware to redirect
www→ apex or vice versa - Future: Could add
api.hoffdesk.comfor machine-to-machine endpoints
Source: Starlette Host routing docs (https://www.starlette.dev/routing/), FastAPI sub-applications (https://fastapi.tiangolo.com/advanced/sub-applications/)