# HoffDesk Domain Architecture **Last Updated:** 2026-04-24 21:34 UTC **Owner:** Matt (Director), Daedalus (Frontend), Socrates (Backend) --- ## Live Subdomains All routed through a single Cloudflare Tunnel on the Beelink, now using **Starlette Host-based routing** to dispatch requests to the correct sub-app. | Subdomain | Port | Service | Status | Purpose | |-----------|------|---------|--------|---------| | `hoffdesk.com` | :8000 | hoffdesk-api (FastAPI) | ✅ Live | Public blog (SEO) | | `notes.hoffdesk.com` | :8000 | hoffdesk-api → notes_app | ✅ Live | Blog admin + content pipeline | | `family.hoffdesk.com` | :8001 | hoffdesk-api → family_app | ✅ Live | Family dashboard | | `api.hoffdesk.com` | :8000 | hoffdesk-api | ✅ Live | JSON API (blog, content, health) | | `cal.hoffdesk.com` | :5232 | Radicale | ✅ Live | CalDAV calendar | | `hook.hoffdesk.com` | :8000 | hoffdesk-api | ✅ Live | Webhook receiver | | `blog.hoffdesk.com` | — | — | ❌ REMOVED | Was duplicate of notes, no DNS CNAME | | `proto.hoffdesk.com` | :8765 | Dev server | ⚠️ Dev only | Retire after family. stable | **Host routing architecture:** ``` Cloudflare Tunnel │ ├── family.hoffdesk.com → family_app (port 8001) │ ├── / → Dashboard (Jinja2) │ ├── /family/login/ → Login page │ ├── /api/today → Calendar + weather + health │ └── /admin/ → Family automation │ └── notes.hoffdesk.com, hoffdesk.com, api.hoffdesk.com, hook.hoffdesk.com → notes_app (port 8000) ├── / → Blog (public) ├── /admin/blog/ → Blog admin suite ├── /api/blog/ → Blog JSON API ├── /api/v1/content/* → Content pipeline └── /webhook → Webhooks / health ``` --- ## Application: hoffdesk-api Two FastAPI sub-apps, one process. Host-based routing via Starlette `Host()` dispatches at the edge. ### family_app (port 8001 — family.hoffdesk.com) ``` family.hoffdesk.com ├── / → Dashboard (Jinja2 template, requires auth) ├── /family/login/ → Login page (public) ├── /auth/login → Session auth POST ├── /auth/me → Session validation ├── /api/today → Calendar + weather + health (requires auth) ├── /api/calendar/upcoming → Calendar events (requires auth) ├── /api/weather → Weather data (requires auth) ├── /api/health → System health (requires auth) ├── /admin/* → Family automation (requires auth) ├── /static/ → Dashboard CSS ├── /family/events/removed → Recently removed events (Bearer token) ├── /telegram/callback → Telegram webhook └── /webhook → Generic webhook receiver ``` ### notes_app (port 8000 — notes.hoffdesk.com, hoffdesk.com, api.hoffdesk.com, hook.hoffdesk.com) ``` notes.hoffdesk.com ├── / → Blog index (public HTML) ├── /article/{slug} → Article page (public HTML) ├── /categories → Category list ├── /tags → Tag list ├── /api/blog/ → Blog JSON API │ ├── /posts → Post listing │ ├── /posts/{slug} → Single post │ ├── /categories → Category list │ ├── /tags → Tag list │ ├── /feed.xml → RSS 2.0 feed │ ├── /sitemap.xml → Sitemap │ └── /admin/* → Blog admin API (requires auth) ├── /admin/blog/ → Blog admin panel (requires auth) │ ├── /login → Admin login page (now session cookie) │ ├── /posts → Post list │ ├── /posts/new → New post editor │ ├── /posts/{slug}/edit → Edit existing post │ ├── /images → Image upload │ └── /content/* → Content pipeline v2 ├── /api/v1/content/ → Content pipeline v2 API ├── /health → Health check └── /webhook → Webhook receiver (hook.hoffdesk.com) ``` --- ## Auth Model (Finalized 2026-04-24) **Session cookies for humans, Bearer tokens for machines. No exceptions.** | Route | Auth Method | Who | Notes | |-------|-------------|-----|-------| | `/` and `/api/blog/*` (public) | None | Anyone | SEO + RSS | | `/admin/blog/*` | Session cookie | Matt (admin) | Redirects to login if unauthenticated | | `/admin/family/*` | Session cookie | Matt, Aundrea (family) | Role check (admin/family) | | `/api/v1/content/*` | Session cookie (browser) or Bearer (API) | Matt | Dual-mode | | Dashboard `/` + `/api/today` | Session cookie | Authenticated users | Requires login | | `/family/login/` | None | Anyone | Public login page | | `/family/events/removed` | Bearer token | Dashboard proxy | Machine-to-machine | | `/telegram/callback` | Internal secret | Telegram bot | Unchanged | | `/webhook` | Internal secret | Cloudflare Worker | Unchanged | ### Credentials | User | Password | Roles | Access | |------|----------|-------|--------| | `matt` | `hoffdesk-matt-2026` | admin, editor, family | Everything | | `aundrea` | `hoffdesk-aundrea-2026` | family | Dashboard only | --- ## Navigation Flows ### Public Users → `hoffdesk.com` ``` hoffdesk.com → Blog index (articles, categories, tags) → Article pages (full posts) → RSS feed (/api/blog/feed.xml) → Sitemap (/api/blog/sitemap.xml) ``` ### Matt (Admin) → `notes.hoffdesk.com/admin/blog/` ``` notes.hoffdesk.com/admin/blog/ → Login → session cookie set → Post list → New Post / Edit Post → Content Pipeline (Magic Wand + Struggle v2) → Image upload notes.hoffdesk.com/admin/family/ → Family pipeline status → Email webhook config ``` ### Aundrea at 7 AM → `family.hoffdesk.com` ``` family.hoffdesk.com → (no session) → redirect to /family/login/ → Login → session cookie set → redirect to / → Calendar card (HTMX, polls /api/today every 30s) → Weather card (HTMX, polls /api/today every 30s) → System health card (HTMX, polls /api/today every 30s) → Recently Removed widget (JS fetch every 60s) ``` ### API Consumers → `api.hoffdesk.com` ``` api.hoffdesk.com → /api/blog/* (public blog data) → /api/v1/content/* (content pipeline) → /family/events/* (family data) → /health (service health) ``` --- ## Architecture Decisions Log | Date | Decision | Made By | |------|----------|---------| | 2026-04-19 | Sovereign stack: FastAPI + HTMX + Tailwind, no SPA frameworks | Matt | | 2026-04-19 | Mobile-first, dark mode default, no toggle | Matt | | 2026-04-20 | Blog at `notes.hoffdesk.com`, admin at `/admin/blog/` | Matt | | 2026-04-21 | Dashboard at `family.hoffdesk.com`, bare domain reserved for landing | Matt | | 2026-04-24 | Session cookies for browsers, Bearer tokens for API/webhooks | Matt | | 2026-04-24 | Roles: `admin` (full), `family` (dashboard only) | Matt | | 2026-04-24 | Host-based routing via Starlette `Host()` for subdomain separation | Socrates | | 2026-04-24 | `family.hoffdesk.com` → family_app (port 8001), everything else → notes_app (port 8000) | Socrates | | 2026-04-24 | Dashboard converted to Jinja2 templates (base.html.j2 + index.html.j2) | Daedalus | | 2026-04-24 | `blog.hoffdesk.com` removed from tunnel config (zombie subdomain) | Matt | | 2026-04-24 | `hoffdesk.com` apex A record added pointing to Cloudflare proxy | Daedalus | --- ## Sprint Priorities (as of 2026-04-24) ### Completed Today - ✅ Host-based routing deployed (Socrates) - ✅ `family.hoffdesk.com` in tunnel + DNS (Wadsworth) - ✅ `hoffdesk.com` A record added (Daedalus) - ✅ Auth unified — session cookies for browsers, Bearer tokens for APIs (Socrates + Daedalus) - ✅ All `?token=` and `X-Admin-Token` refs removed from templates (Daedalus) - ✅ Hardcoded URLs updated: `magic_wand` API_BASE → relative, admin_base Dashboard link → `family.hoffdesk.com` (Daedalus) - ✅ Dashboard login page delivered (Daedalus) - ✅ Dashboard converted to Jinja2 templates (Daedalus) ### Next Sprint - Wire pipeline Phase 2 frontend templates to backend routes (Socrates) - Dashboard Jinja2 → router integration (Socrates — see `JINJA2-HANDOFF.md`) - Retire `proto.hoffdesk.com` (dev server no longer needed) - Build Week View + Conflict Panel + Undo Toast (Family Assistant v1.5) - Public landing page at `hoffdesk.com` --- ## Remaining Pain Points 1. **Dashboard still served as static HTML** (via `read_text()`). Jinja2 templates are ready but router needs updating. See `shared/project-docs/dashboard/JINJA2-HANDOFF.md`. 2. **Pipeline Phase 2 templates not wired.** My struggle-first pipeline templates sit in the filesystem but have no backend routes rendering them. 3. **Template directory fragmentation.** Dashboard templates in `shared/project-docs/dashboard/templates/`, blog in `shared/project-docs/blog/templates/`, admin workspace copies mirroring them. Need a single source of truth convention. 4. **`api.hoffdesk.com` serves HTML** at `/` when you'd expect JSON-only. Works but violates the "each subdomain one job" principle.