Host & tenant routing
The render middleware turns an arbitrary public host into two facts: which tenant the request is for, and which engine backend serves it.
Resolution order
The middleware tries these in order and stops at the first match:
- Platform subdomain. A host shaped like
{tenant}.{platform-host}yields the slug directly. - Non-platform domain. Call
resolveBoxHost(host)against the control plane:match: 'wildcard'with atenantSlug→ use that slug and the box’sbackendUrl.match: 'exact'→ ask the bank backendGET {backendUrl}/tenants/by-domain/{host}for the slug.
- Custom platform domain. Look up the slug via
lookupCustomDomain(host). - Fall through. No tenant; handled as a path-based or platform route.
Tenant resolution on the engine side
The engine resolves a tenant independently for direct API calls
(tenant.middleware.ts):
- Honor the
X-Tenant-Slugheader (set by the render proxy). - Otherwise use the
Hostheader: strip the port, try a custom-domain lookup, then a subdomain pattern match ({slug}.{platform}).
Only active or pending tenants resolve; anything else is treated as not found.
The @CurrentTenant() decorator throws 404 when no tenant is attached, and
@TenantId() extracts the resolved tenant id for handlers.
Header injection
When a tenant is resolved, the middleware rewrites the request and threads headers downstream:
| Header | Purpose |
|---|---|
x-subdomain-tenant | The resolved tenant slug |
x-box-backend-url | The bank engine backend the [tenant] routes fetch from |
x-pb-preview-kind | Preview mode (draft / saved) when applicable |
Content-Security-Policy | frame-ancestors allowing the dashboard to embed Live Preview |
The API client reads x-box-backend-url from request headers, so every data
fetch in the [tenant] route tree targets the correct box automatically — no
per-call configuration.
Caching
| Lookup | Positive TTL | Negative TTL | Timeout |
|---|---|---|---|
resolveBoxHost | 60s | 30s | 2s |
lookupTenantOnBackend | 60s | 30s | 2s |
Negative caching is deliberate: an unclaimed or mistyped host is remembered as “no box” briefly so repeated requests cannot stampede the control plane or a bank backend.