Tenancy
One SlotBattle instance serves several casinos at once. A tenant is one casino: its own battles, its own game allowlist, its own credentials, its own webhook endpoint. You are one tenant on the platform's instance.
What a tenant owns, and what the platform owns
This split defines multi-tenancy in SlotBattle. Check it before promising players something the instance cannot provide.
| Belongs to the tenant | Belongs to the platform (the instance) |
|---|---|
| Battles and seats | The viewer-token signing key |
| Game allowlist | The three lobby windows |
| SoftSwiss backend credentials | Mail configuration |
| Result webhook endpoint and secret | The recorder and its callback secret |
| API keys and their scopes | Storage, CDN, database, Redis |
| Console users and their roles |
You can change everything in the left column and nothing in the right one. A different lobby window is a request to the platform, not a setting you own.
How a tenant is identified on a request
Two surfaces, two mechanisms, no overlap.
The S2S API derives the tenant from the API key. There is no tenant header and no tenant field to send: responses and webhooks echo the id, and nothing accepts it as input.
A tenant id read off a payload turns every resource route into a cross-tenant read for anyone willing to edit a JSON field. Deriving it from the credential makes that class of bug unrepresentable.
The admin console carries the active tenant in an X-Tenant-Id header, alongside the
session cookie. It is a header rather than a claim in the token so that an operator with
access to several casinos can switch between them without re-authenticating. Membership is
checked on every request, so the header states an intent rather than an entitlement.
Cross-tenant reads answer 404
Asking for a battle that belongs to another casino returns 404 Not Found, the same answer as asking for a battle that does not exist.
A 403 would confirm the battle exists, turning a permission error into an enumeration
oracle: probe ids, and the ones answering 403 are real. Every battle-scoped route loads the
battle and checks ownership before it mutates anything, so a cross-tenant write cannot take
effect and then be rejected.
A suspended tenant is the exception and does get a 403. The caller is legitimate, so
naming the reason is correct.
Where a tenant's credentials live
Two places, resolved in a fixed order.
Settings documents, per tenant, editable from the console:
| Category | Scope | Holds |
|---|---|---|
softswiss | Tenant | That casino's backend credentials |
webhook | Tenant | That casino's result endpoint and signing secret |
lobby | Platform | The three lobby windows |
The platform's own credentials are the fallback, and are not yours to change. On an
instance where every casino brings their own, the fallback is left empty, which is why a
missing softswiss document surfaces as bot sessions minted under the wrong casino id rather
than as an error.
Three resolution rules that prevent credential leaks
A complete document wins; an empty one falls back to the environment.
A credential group is all-or-nothing. Filling in your own backend URL but leaving the auth token blank is refused at the write, while an operator is still present to fix it. Half filled, it would send the platform's credential to your endpoint.
A resolution failure is an error, never a fallback. If the settings read fails, or a stored secret cannot be unsealed, the operation fails. Falling back would let a transient database fault present the platform's credential under a casino's identity.
Reading a settings document answers "<field>Set": true|false instead of the value. A
submitted body that omits a secret leaves the stored one alone, because the client could not
have received it. Treating an absent field as "clear this" would let pressing Save wipe a
working credential.
Saved settings are cached briefly and invalidated explicitly on write. With only a TTL, a console save would stay invisible until the entry lapsed: the database correct, the running system still on the old credential, and nothing reporting an error.
The instance-level allowlist
Separately from the tenants table, SLOTBATTLE_TENANTS lists which tenant ids a process will
serve at all. It carries no credential and performs no authorisation; it is a membership list.
A tenant absent from it is unknown to that process even if its database row exists.
Game allowlists
Each tenant has an allowlist of games, and the catalog endpoint returns only the allowed ones.
It does not mean "all games". A newly created tenant with nothing allowed sees zero games and every battle creation fails, which reads as a broken integration. Allowing games is part of onboarding.
The console's game screen answers the whole catalog with a boolean per game, because a screen limited to allowed games could not be used to allow a new one.
Next
- Keys and credentials: the credentials and how they rotate
- What the host controls: the same split, from your side
- Authentication: the API key on the wire