Concepts

Security model

What Thuum protects, what it leaves to you, and the practices that keep a real-time app safe.

What Thuum guarantees#

  • Tenant isolation. A token only ever acts within its own Domain.
  • Secrets at rest. App Keys are stored as keyed hashes; the full key is shown once, at creation.
  • Short-lived credentials. Tokens always expire (1 second to 24 hours, chosen per key) and are checked on every REST request and at every WebSocket handshake.
  • Least privilege per topic. Each key gets exactly the flags its claims grant, per Main Topic, with nothing implied.
  • Immediate revocation for REST. Unlinking a key or changing a claim applies to the next request. Open WebSockets keep their permissions until they reconnect; deleting a key stops new tokens and new connections.

Keep App Keys on servers#

An App Key is a long-lived secret. Treat it like a database password:

  • Store it in your secret manager or environment, never in source control.
  • Never ship it to a browser or a mobile app. Anything in a client bundle is public. Browsers get tokens from your server instead: see Connect browsers securely.
  • Use one key per component (web token route, each backend service), so you can renew or revoke one without touching the others.
  • Rotate with the console's Renew flow: the new key works before the old one is retired.

Give tokens a short life#

A token is a bearer credential: whoever holds it can use it until it expires. Tokens handed to browsers are only needed for the WebSocket handshake, so their key's TTL can be short (30–60 seconds). Backend keys that make REST calls can use longer TTLs and cache the token until shortly before it expires.

Tokens travel in the WebSocket URL (?token=) because browsers cannot set headers there. Always use wss:// in production; the API keeps tokens out of its request logs.

Your users are not Thuum users#

Thuum authorizes App Keys, not the people using your app. Every browser that gets a token from your web-browser key has the same permissions as every other one. Two consequences:

Your token route decides who gets in. Check your own session before issuing a token, exactly as you would before serving any private page. Rate-limit it like a login endpoint.

Anything a browser token may subscribe to, every browser token may subscribe to. Permissions are per Main Topic, and a subscription to a Main Topic receives its whole family: a browser allowed to subscribe to chat can subscribe to chat itself and receive every room. Unguessable room ids do not hide a room from a client that subscribes one level up.

Design for it:

DataApproach
Public within your app (lobby chat, live scores)Browser keys with S (and P if users write) on its Main Topic.
Shared by a group, not sensitive (a team's board)Same, accepting that any signed-in user with a token could read other groups' data. Fine for many apps; decide explicitly.
Private to one user or group (DMs, invoices, health)Keep it off browser-accessible Main Topics. Subscribe on your server, check who may see what, and forward it to the right user over your own channel.

When in doubt, give browsers S on fewer Main Topics and let your backend relay the rest.

Treat every value as untrusted#

Anyone whose key has P on a topic can publish any event or write any state there, with any content. Names, ids and roles inside the data are claims made by the client, not facts.

  • Validate the shape and size of everything you read, and ignore what doesn't fit.
  • Render user content as text, never as HTML.
  • Don't let a client-written state decide something your server should decide (prices, roles, results). Have your server write those with a key browsers don't have, on a Main Topic browsers can only read.

Least privilege, in practice#

  • Keep fallback_data_flags at 0 in production and list Main Topics explicitly.
  • Give browsers no Domain Claim and no topic flags.
  • Grant W on data (purge) only to the backend that cleans up.
  • Use GET /me in tests or at startup to assert that a key has exactly the claims you expect.