Documentation

Strict multi-tenancy

How betool isolates your data, secrets and executions from other organisations.

Isolation between organisations is the foundational invariant of betool. Here is how it is enforced, layer by layer.

At the database level

Business tables carry an org_id column that ties each row to its organisation (most often constrained by a foreign key to organisation(id)).

All server-side queries go through store methods that systematically filter by the org_id of the current session. No code path exposes an arbitrary query; no client can force a different org_id.

For the small number of resources that can be shared across organisations (typically platform-level shared LLM accounts), an is_shared BOOLEAN NOT NULL DEFAULT FALSE column enables explicit opt-in. Rules:

  • Any read is authorised with WHERE org_id = %s OR is_shared = TRUE.
  • Any mutation of an is_shared = TRUE row requires the hyperadmin role (platform operator).
  • Any mutation invalidates the cache globally — no per-org divergence.

At the file storage level

Files are kept in object storage (local disk or cloud bucket depending on the deployment), under a space dedicated to each organisation:

orgs/
└── <org_id>/
    ├── files/<channel>/<exchange_id>/<file>    ← files received by the channels
    └── fixtures/                               ← benchmark fixtures

Keys are built server-side (sanitised file name, anti path-traversal). Any read of a file on behalf of a user, a tool or an agent goes through a validation that refuses a key outside the current organisation's orgs/<org_id>/files/ space.

At the cache level

Application caches (for example LLM provider resolution) are keyed by organisation. When an is_shared = TRUE resource is mutated, the cache is invalidated globally — there is no window during which an org could see a stale value.

At the secrets level

Credentials (LLM keys, operator auth tokens, webhook secrets) are stored server-side, attached to each org. They are never returned in plaintext by the API: GET routes only indicate whether they are present (for example has_api_key: bool).

At the execution level

When a pipeline runs:

  • Operators only have access to the external accounts declared within their org.
  • LLM models are resolved in the order own > shared (auto fallback) — always.
  • Attached files are only readable within the current org's storage space.

If an operator attempts to access a resource outside its scope, the access is refused with an explicit error — not a silent timeout.

Practical checklist

Whenever a new shareable resource is added, the team answers these 4 questions:

  1. If a second backoffice org appears, does my code behave correctly?
  2. If I mutate a shared row, are all per-org caches invalidated?
  3. Do my secrets remain strictly server-side?
  4. Does my GET allow other orgs' pickers to list the resource without exposing the secret?

If any answer is no, the resource is not shipped.