# 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`** ```python 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`** ```python """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: ```python # 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: ```python # 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 1. **Static assets:** Confirm paths for dashboard vs blog static files 2. **HTMX base URLs:** May need subdomain-aware base URL in templates 3. **Design tokens:** Verify `design-tokens/` paths work across subdomains 4. **Testing:** Test cross-subdomain navigation once live --- ## Wadsworth Coordination Points 1. **Cloudflare Tunnel:** Verify DNS records for `notes.hoffdesk.com` point to same tunnel 2. **Certificate:** Ensure SSL covers `*.hoffdesk.com` wildcard 3. **Monitoring:** Add health checks for each subdomain endpoint 4. **Rollback:** Keep `main_legacy.py` ready 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_path` handling should work automatically for sub-apps - Consider custom middleware to redirect `www` → apex or vice versa - Future: Could add `api.hoffdesk.com` for machine-to-machine endpoints --- **Source:** Starlette Host routing docs (https://www.starlette.dev/routing/), FastAPI sub-applications (https://fastapi.tiangolo.com/advanced/sub-applications/)