Strict multi-tenancy
Isolation between organisations is the foundational invariant of betool. Here is how it is enforced, layer by layer.
At the database level
Every business table carries an org_id column (UUID), indexed and 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 filesystem level
Each org has its own storage root:
<data_root>/
└── orgs/
└── <org_id>/
├── fixtures/
├── uploads/
├── knowledge/
└── exchanges/
Server helpers (org_*_dir(org_id)) resolve these paths. No business code can write outside an organisation's root.
At the cache level
All application caches (LLM provider resolution, current contexts, parsed files) are per-org. 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 in each org's vault. They are never returned in plaintext by the API: GET routes only return has_api_key: bool.
On the Enterprise plan, the vault can be connected to HashiCorp Vault, AWS Secrets Manager or Azure Key Vault.
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 under the current org's root.
If an operator attempts to access a resource outside its scope, the error is explicit (OrgScopeViolation) — 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.