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 = TRUErow 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:
- If a second backoffice org appears, does my code behave correctly?
- If I mutate a shared row, are all per-org caches invalidated?
- Do my secrets remain strictly server-side?
- 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.