Tenancy & isolation
RuleFlow is multi-tenant by default. Every control-plane operation is scoped
to the caller’s verified tenant claim — there is no request in the API
where tenant is inferred from a URL path, a query string, or a body field.
The whole model reduces to one sentence: every operation acts only as the tenant in the verified token. Its strength depends on exactly two things: the token cannot lie about the tenant, and no code path can forget the filter. Both are structurally enforced — see below.
The tenant boundary is the token, never the request
A request with no tenant claim (or an empty one) is rejected 403 on
every tenant-scoped route — there is no default tenant and no god-tenant. A
caller cannot ask for another tenant’s data by changing a path parameter;
the only tenant they can act as is the one in their token.
project is a label within a tenant, taken from the path — not a
security boundary. There is no isolation between two projects of the same
tenant; any of that tenant’s members can read any of its projects. Two
tenants may both use project=lending without collision (the tenant is the
partition).
Three enforcement layers
- Identity (Keycloak). The
tenantclaim’s provenance: admin-provisioned for direct users, hardcoded per-IdP at SAML brokering onboarding time for federated users (the SAML assertion’s owntenantattribute is ignored — an IdP cannot assert a tenant other than the one it was onboarded for). Service-account (machine) tokens carry no tenant claim at all, and are therefore rejected on every tenant-scoped route by construction. - Contract (the
Store). Every store method takestenantas a required parameter — the persistence interface documents “all methods are scoped to a tenant,” and both the Postgres and in-memory implementations filter by it. No method exists that reads across tenants: a handler physically cannot express a cross-tenant query. - Infrastructure. Tenant is the leading dimension everywhere data
rests — the DynamoDB partition key is
TENANT#<t>#PROJ#<p>for executions/human tasks/connector bindings, search queries carry a mandatory tenant filter, and relational indexes lead withtenant.
Cross-tenant access is “not found,” not “forbidden”
Fetching another tenant’s artifact or release by id returns a typed
not-found error → HTTP 404, the same response whether the id doesn’t exist
at all or belongs to someone else. This means the existence of another
tenant’s objects is never revealed to a caller who doesn’t have access to
them.
RBAC is orthogonal to tenancy
Realm roles (approver, admin, …) gate what an authenticated caller may
do; tenancy gates whose data they can do it to. A role never widens tenant
scope — a privileged deploy still resolves the release within the caller’s
own tenant.
Isolation tiers
| Tier | What is shared | Status |
|---|---|---|
| Pooled | Shared services + database; row/partition-level scoping by tenant claim | The only tier that runs today; default for all tenants |
| Bridge | Shared services; per-tenant schema/database | Tooling exists; no app-level routing yet |
| Silo | Dedicated stack per tenant | Deployment tooling exists; nothing deployed yet |
Because authorization is always “act as the token’s tenant,” moving a specific tenant from pooled to bridge/silo is a data-placement change, not an authorization-model change — the API surface and authored rules are unaffected either way. Pooled is the only tier live in production today; bridge and silo are a committed escalation path for a customer that needs stronger isolation, not a flag you flip per tenant yet.
Dev/insecure mode
A local development mode exists that parses a JWT without verifying its
signature, but it still requires the tenant claim — local runs behave
identically with respect to scoping. It must never run in a shared or
production context; it trusts any unsigned token’s tenant claim.
See Security § Isolation model for the deeper security-review writeup (shared blast surface, mitigations, and what remains open).