Skip to main content

Host-Native Custom Domain Routing

This retrospective covers the July 13, 2026 follow-up to the Custom Domain White-Label Routing Incident. The July 12 fix stopped canonical landing leakage, but the browser-visible custom-domain experience still leaked the canonical tenant path in URLs such as https://joossameng.com/joossameng?auth=login.

The follow-up made the custom domain truly host-native: joossameng.com identifies the tenant by host, not by a tenant path inside that host.

Context

Lumie has two valid public URL models:

URL modelExampleMeaning
canonical tenant pathhttps://lumie-edu.com/joossamengtenant identity comes from the first path segment
white-label hosthttps://joossameng.com/tenant identity comes from the host

The July 12 incident fixed the safety problem first. Unknown or unresolved custom domains stopped falling through to canonical Lumie marketing content. That still left a UX and routing-model problem: configured custom domains could expose canonical tenant paths after login or auth refresh.

The current source surfaces are:

  • lumie-frontend/proxy.ts
  • lumie-frontend/src/shared/lib/serverCustomDomain.ts
  • lumie-frontend/app/page.tsx
  • lumie-frontend/app/[customId]/page.tsx
  • lumie-frontend/src/shared/lib/middlewareCustomDomain.test.ts

Impact

The follow-up did not address a new data leak. It addressed a branding and routing-boundary gap:

  • custom-domain users could see an internal canonical path shape in the browser;
  • auth URLs such as ?auth=login needed to remain on the host root;
  • frontend middleware tests alone were not enough because rendering crossed middleware, server components, and auth state;
  • production verification had to prove the new image was actually deployed, not just that git contained the fix.

The affected tenant in the source note was joossameng.

What Changed

Host-native root rendering

lumie-frontend/app/page.tsx now uses trusted custom-domain headers to decide whether / should render a tenant landing or the canonical Lumie marketing page.

That gives these outcomes:

RequestExpected behavior
https://joossameng.com/render Joossameng tenant landing at root
https://joossameng.com/?auth=loginopen login flow without adding /joossameng
https://lumie-edu.com/render canonical Lumie marketing home
https://lumie-edu.com/joossamengrender canonical tenant landing

Tenant paths blocked on custom hosts

lumie-frontend/app/[customId]/page.tsx remains the canonical-host tenant route. The proxy blocks /${customId} on configured custom domains with 404, so the custom host no longer behaves like a browser-visible alias for the canonical path.

This is the invariant:

configured custom domain + tenant path == route-model leak

The correct custom-domain URL keeps the tenant identity in the host.

Trusted headers bridge middleware and server components

The middleware strips spoofable tenant headers and injects trusted values only after the configured host map resolves the domain. Server components then read those trusted headers through serverCustomDomain.ts.

The important boundary is that server components do not re-run the runtime by-domain lookup. They consume the result of the middleware routing decision.

Verification Gates

The follow-up was as much about verification as implementation.

Unit boundary

lumie-frontend/src/shared/lib/middlewareCustomDomain.test.ts covers the middleware contract:

  • configured custom-domain root passes without canonical rewrite;
  • /login redirects to /?auth=login;
  • /auth/refresh is allowed on configured custom domains;
  • /${customId} is blocked on configured custom domains;
  • unresolved custom-domain roots fail closed.

Rendering boundary

The rendering contract crosses several files:

proxy.ts
-> trusted custom-domain headers
-> serverCustomDomain.ts
-> app/page.tsx
-> tenant landing or canonical marketing

That is why browser-level smoke testing remained necessary even when middleware tests passed.

Deployment boundary

The source incident record also calls out a verification trap: merged code is not deployed code. The production behavior only changed after the built image reached the deployment and the running pods reflected that tag.

For routing fixes, verification needs both:

  • source-level tests for the proxy and rendering decision;
  • production smoke tests against the real host after rollout.

What Went Well

  • The follow-up separated the safety fix from the UX/routing-model fix.
  • The final model treats host as identity on custom domains, which is easier to reason about than path aliasing.
  • Tests now pin the middleware cases that previously regressed.
  • The work explicitly verified deployment state instead of trusting commits alone.

What Went Poorly

  • The first fix still allowed user-visible tenant-path leakage.
  • Middleware-level verification did not fully cover server-component rendering.
  • Auth and refresh flows had to be rediscovered as separate custom-domain URL shapes.
  • Some production uncertainty came from image rollout timing rather than code behavior.

Lasting Invariants

  • On a configured custom domain, the host is the tenant identity.
  • Canonical tenant paths are valid on the canonical host, not on the custom host.
  • Auth, admin, dashboard, API, and refresh flows must preserve trusted tenant context without runtime lookup on every request.
  • Unknown custom-domain hosts must fail closed and must not render canonical Lumie marketing content.
  • Host-native root rendering needs browser-level verification because it crosses middleware, server components, and auth flow.

Action Items

ActionOwner / surfaceStatusEvidence
Keep custom-domain route tests covering root, login, refresh, tenant path, and unresolved host cases.lumie-frontend/src/shared/lib/middlewareCustomDomain.test.tsDone / maintainTest surface named in the source note.
Smoke test the real custom host after routing deployments.frontend rollout processRequired for future changesThe incident record showed deployment-state ambiguity.
Keep server components dependent on trusted middleware headers, not ad hoc by-domain lookup.serverCustomDomain.ts, app/page.tsxOngoing invariantHost-native rendering depends on this split.