An installation can now serve the Dashboard and the API on sibling
hostnames (dash./api.) beside the default same-origin topology, from one
image and with configuration alone.
- The Dashboard learns its API origin at run time: the built page carries
one empty <meta name="ekklesia-api-origin">, which the API fills with its
identity base URL's origin at startup, before computing the page's ETag.
The shell accepts only a canonical http(s) origin and otherwise fails
closed without making a request. The build-time EKKLESIA_API_BASE_URL is
gone from the bundle; the development server injects the same element.
- The display host stays on the API's origin, so its device cookies,
requests, and offline cache are unchanged.
- The API trusts its own origin (trusted origins plus the identity base
URL), as Better Auth already does, so the display page's mutations pass
the origin check without operators listing it.
- Doctor validates EKKLESIA_DASHBOARD_ORIGIN exactly as the API's startup
does (it previously skipped it), reports the topology without printing
hostnames, and warns when split hostnames are not siblings.
- The module UI contract states the CORS header limit (safelisted headers
plus Content-Type, safelisted response headers only).
- Reference deployment: Caddyfile.split.example (Dashboard hostname serves
/ and its script; API hostname serves /api/*, /display/*, and the
display script; each refuses the other's paths) and
EKKLESIA_DASHBOARD_HOSTNAME in the proxy overlay and env examples.
- The deploy smoke test runs either topology (CI runs both). The split run
installs the module template as a test-only module, checks per-hostname
routing and exact CORS, then drives a digest-pinned headless Chromium
over the DevTools protocol, with no added dependency: sign-in on the
Dashboard hostname, the session cookie host-only on the API hostname,
the module UI imported cross-origin and its credentialed request
succeeding, the display and its service worker on the API hostname, and
sign-out.
ADR 0045 is marked Accepted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Serve surfaces/shell's built Dashboard from apps/api on the same origin
as /api (ADR 0044), exactly as the display host is served: a closed set
of files read once at startup, never a static directory, never a path
built from request input.
- The shell exports @ekklesia/shell/dashboard-assets (the exact built
index.html and script URLs), emits the script at
assets/ekklesia-dashboard/app.js, and index.html names that absolute
path so dev and production serve the same URLs.
- GET / and GET /assets/ekklesia-dashboard/app.js answer from a frozen
snapshot: no-cache with a strong ETag and 304 on match. Source maps
are never served; every other path is the ordinary sanitized 404.
- The page sets nosniff, X-Frame-Options: DENY, Content-Security-Policy:
frame-ancestors 'none' (that directive only), and Referrer-Policy:
no-referrer, on 200 and 304 alike.
- A missing or empty shell build fails startup with a fixed
DashboardHostAssetError before any module code is imported.
- The If-None-Match comparison moves to conditional-request.ts, shared
with the display host routes.
- The emailed-link token stripping moves from browser/main.tsx into
consumeAuthLanding (location and history injected) so it is tested;
behavior is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>