# Tenancy

A **tenant** is one branded deployment: its own name, logo, theme, enabled
venues, feature flags, fee schedule, and users. One platform serves many; the
API surface is identical for all of them.

## How the tenant is resolved

Resolution runs **before authentication**, in this order:

1. **`X-Tenant-Slug` header** — required for server-side integrations. It is
   trusted only when it names a real tenant; an unknown slug is silently
   ignored rather than rejected, and resolution falls through. A slug naming a
   **suspended** tenant still resolves — it is rejected afterwards with
   `410 {"error":"tenant_suspended"}`, not skipped.
2. **Origin / Host** — matched against the tenant's registered origins. This is
   how a browser front end on a branded domain resolves without a header. When
   no `Origin` is present the `Host` header is used instead.
3. **Platform default** — only when the request carries neither `Origin` nor
   `Host`, or when the origin is the platform tenant's own registered origin.

Anything else is rejected with `404 {"error":"unknown_tenant_origin"}`.

That last point is the wall every integrator hits first. `Host` is mandatory on
every HTTP request, so a server-side call to a shared host like
`api.troncharts.xyz` never falls through to rule 3 — it presents an origin that
belongs to no tenant and 404s before your credential is even read.

**Every server-side call must send `X-Tenant-Slug`, or come from an origin
registered on your tenant.** There is no third option.

```bash
curl -s https://api.troncharts.xyz/api/v1/venues \
  -H "authorization: Bearer $TOKEN" \
  -H "x-tenant-slug: $TC_TENANT_SLUG"
```

Because an unknown slug is ignored rather than rejected, a typo in the slug
gives you the same `404 unknown_tenant_origin` as sending no header at all.

A small set of paths skip the gate entirely, because the caller has no tenant to
offer yet: the bearer mint and revoke (`/api/auth/api-token`,
`/api/auth/api-token/revoke`), `/api/v1/discovery`, self-serve signup
(`/api/public/tenants/*`), the public firm-transparency reads, and the docs
themselves. Everything else — including `POST /api/auth/bootstrap` and all of
`/api/v1/*` — is gated.

### What the header does and does not do

`X-Tenant-Slug` gets you **past the origin gate**. It does not choose which
tenant your credential acts as, and it cannot widen a credential.

A bearer token acts under its credential's own tenant, read from the credential
row on every request; the tenant the request resolved to is deliberately never
consulted for scope. So a bearer that reaches a host belonging to
another tenant still acts as its own tenant — the request is not rejected, the
header is simply not load-bearing for scope. The one case that is rejected
(`403`) is a token whose pinned claim disagrees with its credential row, which
happens only if the credential was moved between tenants after the token was
minted.

## What an operator controls per tenant

| Area | Controls |
| --- | --- |
| Brand | Display name, logo, favicon, support email |
| Theme | Colour tokens, typography, radius — light and dark |
| Markets | Which venues and symbols are enabled |
| Features | Per-tenant feature flags |
| Fees | The tenant's fee schedule |

Read the effective configuration back at any time — useful for mirroring it
into your own front end:

```bash
curl -s https://api.troncharts.xyz/api/v1/tenants/me \
  -H "authorization: Bearer $TOKEN" \
  -H "x-tenant-slug: $TC_TENANT_SLUG"
# → { id, displayName, branding, theme, flags, venues, plan, … }
```

Sub-resources: `/api/v1/tenants/{id}/branding`, `/theme`, `/flags`, `/venues`,
`/plan`. These are read-only unless your credential carries `tenant:config`.

## The isolation rule

Tenant isolation is the load-bearing rule of the platform: **every read and
every write is filtered by the caller's tenant, at the query, not in the
application layer.** This holds for REST and for the WebSocket channels — a
socket is scoped to its tenant at handshake and is torn down if that access is
revoked mid-connection.

The practical consequence for you: you never have to filter by tenant yourself,
and you can treat any row you can see as yours.