Documentation

Strict multi-tenancy

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

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 = TRUE row 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:

  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.