Skip to main content

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:

HostServesWhere it runs
ethioconnect.netcustomer site (docs-site/) — marketing, pricing, docsFirebase Hosting
tenant.ethioconnect.nettenant dashboard (admin/dashboard/)Kubernetes
admin.ethioconnect.netplatform/operator dashboard (admin/ops-dashboard/)Kubernetes
api.ethioconnect.netcontrol-plane RESTKubernetes

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.

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 to must-revalidate. Verified against the live site.
  • **/*.html matches 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's max-age=3600 default, 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.net is provisioned and renewed by Firebase; there is no certificate to rotate in deploy/.
  • 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.