All docs

Docs · Deploy & operate

Tenants

one chassis, many isolated worlds

In Thanks, Computer, every piece of state belongs to a tenant — an isolation boundary for stacks, domains, secrets, people, and usage. This page is the model, and how to set it up.

You’ve been using it all along: a fresh chassis seeds a tenant named default, and everything in the quickstart ran inside it. That’s the design — isolation isn’t a feature you turn on later, it’s how the chassis thinks. A single team can ignore tenancy entirely; the moment you host a second team, a client, or a product, the walls are already there.

What lives inside a tenant

Everything, keyed at the database row:

ResourceThe boundary
Stacks & rulesA stack name is unique per tenant; tenant A’s rules never fire for tenant B’s events
Hostnames & domainsHostname bindings, delegated DNS zones, and DKIM keys are tenant rows — one tenant’s claim 409s another’s
SecretsScoped (tenant, stack, name); materialization can’t cross the line
TracesTenant-attributed; the admin API only serves them under /v1/tenants/{slug}/…
CronEach tenant with a _cron stack gets its own tick envelope
Usage & fuelEvery request’s spend is attributed to its tenant — the quota/billing dimension
Outbound mailPer-tenant rate limits, and the from-domain must be that tenant’s verified hostname
PeopleActors hold per-tenant memberships with per-tenant capabilities

How an event gets its tenant — and keeps it

Routing resolves the tenant before any rule fires — by hostname, mail recipient, or listener — and stamps @tenant on the envelope. Then it’s pinned: immutable for the life of the request. Rules and ops can’t overwrite it (the envelope guard rejects the write), and usage accounting reads the pin, not the envelope — a misbehaving rule can’t bill its work to someone else.

Events that match nothing land in the system tenant (_sys), where boot rules may assign them to a real tenant exactly once. Tenant slugs starting with _ are reserved for the chassis.

People: memberships, not global roles

An actor’s capabilities are per tenant — admin of acme, read-only in beta, nothing anywhere else. The admin API enforces this at the path: under /v1/tenants/acme/…, a signed caller’s capabilities are re-resolved from their acme membership (no membership, no access), and a browser session minted for one tenant won’t replay against another. Invitations are tenant-scoped and can’t grant capabilities the inviter doesn’t hold. Operators with super_admin see across tenants — that’s the one global role.

Setting one up

txco auth tenant create acme                                # the tenant
txco auth invite --tenant acme --label "alice"              # token for a teammate
txco auth tenant hostnames add acme.example.com --stack acme/web
txco auth tenant secrets set STRIPE_API_KEY --tenant acme
txco apply --tenant acme                                    # deploy stacks into it

--tenant defaults to default (or your profile’s default), so single-tenant workflows never type it. txco auth tenants lists what you can see; txco auth memberships lists where you belong.

Operating tenants

Suspension is a clean kill switch: txco admin tenant suspend acme --deny-status 402 --deny-reason payment_required makes the chassis answer that tenant’s traffic with your chosen denial — while still attributing the attempts to the tenant for the record. resume lifts it. Deleting is a soft revoke: routing and cron stop, history and audit rows remain.

Outbound-mail rate limits are tracked per node (a runaway-loop valve, not fleet-wide accounting), and hostname-ownership verification is permissive by default — set --require-hostname-verification in production so unverified bindings don’t route.

Edit this page · View as markdown