📄 DOMAIN-ARCHITECTURE.md 9,543 bytes Apr 24, 2026 📋 Raw

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

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.