Module XX — multi-tenancy & org management
Module XX is not a service that hangs off the engine — it is a property of the engine itself. There is no separate tenancy module to attach; instead, the core data model carries a tenant boundary on every entity and the store enforces it below every query. This page is the reference for what that boundary guarantees today, and for the parts of org management that are still design-stage.
What it is
Section titled “What it is”Multi-tenancy lives in the Engine layer (layer 0), alongside the platform’s own API
(module XIX), because retrofitting isolation onto a running data model is the kind of
change you cannot do safely later. Every core entity carries a tenant_id, and a
caller never passes one as a free parameter: it fixes the tenant once and receives
a scope whose repositories are already bound to it. There is no vocabulary in the
API to cross tenants — that absence is the first isolation barrier, before any
database mechanism. The privileged cross-tenant scope (creating an org, listing orgs,
dropping a tenant) is reachable only by the engine’s own startup, never by a
module.
The contract and the entities
Section titled “The contract and the entities”The tenant model is owned by the data-model contract, not by a per-module
schema. The root entity is the Org, which is the tenant: when the engine seeds
an org, its identifier becomes the tenant identifier and the org’s own audit chain is
established at the same moment. Every other core entity — agents, sessions, resources,
identities, policies, cost records, findings, deployments, the access map and the
audit ledger — is created inside a tenant scope and stamped with that tenant on
write; the caller cannot override it.
Isolation is enforced at the query layer, by deployment:
- On PostgreSQL, each table carrying
tenant_idruns underFORCE ROW LEVEL SECURITYwith atenant_isolationpolicy bound per transaction. A transaction that fails to bind a tenant raises an error rather than silently returning zero rows (fail-closed). The application role is non-superuser and never hasBYPASSRLS, andFORCEbinds the policy even for the table owner. Ownership is a deployment choice: the default single-role install leaves the application role as database owner — RLS still binds it, but an owner can alter its own tables, so that posture is tamper-evident rather than owner-proof. The hard privilege boundary — an application role that is also non-owner — comes from the split owner/app topology, where a separate owner role runs provisioning and the application role receives only the DML it needs. - On SQLite (the single-node deployment) there is no row-level security; the equivalence comes from two facts — the only path to the database is the descriptor-generated SQL that always appends the tenant predicate, and tripwire triggers abort any write whose tenant does not match the pinned scope.
A startup self-test queries the live isolation guards after migrating and
refuses to open the store if any table carrying tenant_id is unprotected — so a
forgotten guard on a new table becomes a boot failure, not a silent leak.
What it consumes and produces
Section titled “What it consumes and produces”Module XX has no event-bus surface and no actuation. It does not consume edge.observed,
emit findings, or call any provider — it is the substrate the other modules write
through. Its only observable effect is structural: every entity any module persists
is already tenant-scoped, and every mutation on an audited entity appends to that
tenant’s hash-chained audit ledger within the same transaction.
Related
Section titled “Related”- Modules catalog — where module XX sits and its honest actuate status.
- Identity, permissions & governance — roles and delegated authority within a tenant.
- Architecture overview — the engine layer and the general data model.
- Event bus reference — the per-tenant audit ledger every mutation appends to.
- Honesty & limits — what is built today versus design-stage.