ADR 0042 — Customer Site on Firebase Hosting at ethioconnect.net
Status: Accepted Context date: 2026-08-17 Builds on: ADR 0022 (dashboard serves at the root of its own origin)
Context
docs-site/ is the customer-facing site — marketing, pricing, and the generated
API/architecture documentation. It is the only front-end in the repo that is
fully static: Docusaurus emits plain HTML/CSS/JS into docs-site/build, with
no server runtime, no per-request rendering, and no backend calls it must make
itself. The navbar's session check against the tenant dashboard happens in the
browser against customFields.appUrl.
The rest of the platform (Go services, portal/, admin/dashboard/) runs on
Kubernetes per ADR 0008. Putting the customer site through that same path would
mean an ingress, a TLS certificate, an nginx image, and a pod whose only job is
to serve files that never change between deploys — an availability liability for
the one asset a prospective customer sees first.
Decision
1. The customer site is hosted on Firebase Hosting, not in the cluster
Static output is uploaded to Firebase Hosting and served from its global CDN.
The target is Firebase project interactive-comment-sect-563db (display
name communication-baas), site interactive-comment-sect-563db, default URL
https://interactive-comment-sect-563db.web.app. The opaque project ID is a
historical artifact of the project it was created under; it is referenced only
from .firebaserc and never appears in a customer-visible URL once the custom
domain is attached.
Config lives in docs-site/firebase.json and docs-site/.firebaserc, keeping
the site self-contained: cd docs-site && firebase deploy --only hosting, or
make docs-deploy from the root, which builds first.
This is deliberately not where the rest of the platform goes. Firebase Hosting is correct here precisely because the site is static and has no data of its own. Anything with a server runtime, tenant data, or media stays in the cluster.
2. Apex domain for the site; dashboards on their own subdomains
ethioconnect.net is now the real domain, replacing the *.example.com
placeholders. The production host map is:
| Host | Serves | Where it runs |
|---|---|---|
ethioconnect.net | customer site (docs-site/) — marketing, pricing, docs | Firebase Hosting |
tenant.ethioconnect.net | tenant dashboard (admin/dashboard/) | Kubernetes |
admin.ethioconnect.net | platform/operator dashboard (admin/ops-dashboard/) | Kubernetes |
api.ethioconnect.net | control-plane REST | Kubernetes |
The two dashboards keep separate origins per ADR 0021 and serve at the root of
those origins per ADR 0022. docusaurus.config.ts sets url to the apex and
defaults the production APP_URL to https://tenant.ethioconnect.net;
ETHIOCONNECT_APP_URL still overrides at build time.
Separate origins mean the marketing site can never read a dashboard session cookie, a compromise of the static site cannot reach dashboard state, and an operator session is not reachable from a tenant-facing origin.
trial/ is a local two-users-in-one-tab connection test rig and is not
hosted on any public origin.
2a. The dashboards stay in the cluster — Firebase is not an option for them
Both dashboards are output: 'standalone' Next.js with middleware.ts doing
session gating and a server-side /api/[...path] BFF that forwards the httpOnly
cookie to admin-gateway over a ClusterIP address. Firebase Hosting serves
static files only, so it cannot run them at all; Firebase App Hosting could, but
only by exposing admin-gateway to the public internet — dissolving the exact
guarantee that BFF exists to provide — and by routing every request GCP→AWS,
since the platform runs on EKS. They keep their existing Helm charts and
Ingresses.
The dividing line is not "static vs. dynamic" for its own sake: it is whether the surface needs a private path to the control plane. The customer site does not; the dashboards do nothing else.
2b. Cookie isolation now matters more, not less
tenant_token and admin_session must remain host-only cookies (no
Domain= attribute). With all three surfaces under the registrable domain
ethioconnect.net, a Domain=.ethioconnect.net cookie would be sent to the
Firebase-hosted marketing site as well as to both dashboards. The Ingress adds
no cookie-domain rewrite and no affinity annotations, and must not gain one.
The one deliberate cross-origin hole is the customer site's navbar session
check: admin/dashboard's BFF grants CORS to SITE_ORIGIN on
/admin/v1/auth/session only, and only for that exact origin. That value is
now https://ethioconnect.net. The cookie rides along without any Domain=
widening because apex and subdomain are same-site.
Note the asymmetry this creates, which cost real debugging time: the BFF reads
NEXT_PUBLIC_SITE_URL at runtime (server component — a ConfigMap change
takes effect), while Sidebar.tsx is a client component whose copy is inlined
at image build time. The Dockerfile therefore takes it as a build arg. Setting
only the ConfigMap fixes CORS but leaves the sidebar linking to the wrong host.
3. Cache and security headers are declared in firebase.json
The catch-all ** block carries max-age=0, must-revalidate plus HSTS,
X-Content-Type-Options, X-Frame-Options: DENY, and a
strict-origin-when-cross-origin referrer policy. A later /assets/** block
overrides Cache-Control to immutable for Docusaurus's fingerprinted bundles.
Two non-obvious constraints are encoded there and must survive edits:
- Order matters, last match wins. When two blocks match one request,
Firebase applies the last block's value for a given header key. Putting
/assets/**first — the intuitive order — silently downgrades every bundle tomust-revalidate. Verified against the live site. **/*.htmlmatches nothing here. Docusaurus serves clean URLs (/,/pricing/,/docs/adr/), and Firebase matches header globs against the request path, not the file on disk. An HTML-only cache rule therefore never fires and every page falls back to Firebase'smax-age=3600default, hiding a deploy for an hour. The revalidation rule lives on**for that reason.
4. Preview channels for review, not a staging cluster
make docs-preview publishes to a Firebase preview channel with a 7-day expiry
and a throwaway URL. Content changes get reviewed on a real CDN without
provisioning a staging environment.
Consequences
- The customer site's availability is decoupled from the cluster's. A cluster outage does not take down the marketing and docs pages.
- TLS for
ethioconnect.netis provisioned and renewed by Firebase; there is no certificate to rotate indeploy/. - Deployment is a second, separate path from the platform's. It is scripted
(
make docs-deploy) so it does not drift into a manual ritual, but CI must invoke it explicitly — a cluster deploy does not update the site. - Firebase Hosting serves static files only. If the site ever needs server-rendered or authenticated pages, this ADR must be revisited; the answer would be Cloud Run or the cluster, not Hosting rewrites.