Tenancy Spec
Status: Draft Implements: ADR-OS-008 — Tenancy Model; Core Principles Principle 11 (Open by default, sovereign by design) Authority for: the definition of a tenant and its relationship to a business instance; the control-plane and data-plane boundary; the tenant-scoping rules every control-plane table must meet; the cross-plane connectivity requirements; the single-tenant variant; the data plane hosting declaration. Not authority for: identity (organisations, users, roles, credentials, delegated authority), which belongs to the Identity and Access Spec; metering, the billing unit and entitlement enforcement, which belong to the Metering and Entitlement Spec; table definitions, which stay in the Agent DB Schema.
Overview
Synedrix OS has had no model of who owns a record. A business instance was a deployed runtime, which in practice meant a stack per customer, and the memory fabric and the goal specifications each scoped by a different key. ADR-OS-008 decided the shape: a tenant is the isolation boundary above the business instance, the control plane is pooled under row-level security, and the data plane is never pooled. This document turns that decision into rules a schema, a service and a test can be held to.
It states what must be true. It does not state how identity works, how usage is counted, or what the tables look like.
1. Terms
| Term | Meaning |
|---|---|
| Tenant | The contracting organisation. The unit of isolation, entitlement and metering |
| Business instance | The configured deployment for a company, team or business unit, per the Taxonomy & Platform Spec. Belongs to exactly one tenant |
| Control plane | The shared coordination services: agent registry, work graph, policy, approvals, audit index, entitlement, usage ledger and identity records |
| Data plane | The per-tenant runtime and memory substrate: n8n, LangGraph, Redis, Qdrant, Neo4j, QuestDB, Langfuse, PostHog and the Grafana stack, together with its ingress and egress edge, which carry events to and from it: the outbound Hookdeck Outpost, which is deployed per tenant, and the inbound Hookdeck Event Gateway, a vendor-operated service of which each tenant has its own dedicated sources, connections and destinations (see §3 Edge paths) |
tenant_id | The outer scope key. Opaque, immutable once issued, and never reused |
business_instance_id | The inner scope key. Unique within a tenant |
| Data plane operator | The party that runs the data plane: the customer (self-operated) or Synedrix (managed) |
2. Hierarchy
- A tenant owns one or more business instances. A business instance belongs to exactly one tenant and cannot be moved between tenants.
tenant_idis the outer key in every scope tuple.business_instance_idis the inner key and is meaningful only together with itstenant_id. The memory fabric’s rule thattenant_idis mandatory and always first is unchanged.- Where a document or example uses one value for both keys, the values are two identifiers that happen to coincide. They are not one identifier.
- A record that belongs to a tenant but to no single business instance carries
tenant_idand a nullbusiness_instance_id. A record never carries abusiness_instance_idwithout itstenant_id.
The hierarchy and its scope keys:
flowchart TB
T["Tenant: tenant_id, the outer key"]
B1["Business instance: business_instance_id, the inner key"]
B2["Business instance: business_instance_id, the inner key"]
R1["Record: tenant_id and business_instance_id"]
R2["Tenant-level record: tenant_id and a null business_instance_id"]
T -->|"owns one or more"| B1
T -->|"owns one or more"| B2
B1 --> R1
T --> R2
3. Plane boundary
| Plane | Holds | Isolation | Never |
|---|---|---|---|
| Control plane | Registry, work graph (runtime.*), policy, approvals, audit index, entitlement, usage ledger, identity records | tenant_id and row-level security on every tenant-owned table | Is shared by two tenants without tenant_id scoping on every tenant-owned table |
| Data plane | The components named in §1 | A separate deployment per tenant | Is shared by two tenants, whoever operates it. For the vendor-operated Hookdeck Event Gateway, the tenant’s dedicated configuration is the unit that is never shared (see Edge paths) |
Edge paths. The Hookdeck ingress and Outpost egress paths belong to the data plane’s edge, not to the control plane. The Hookdeck inbound path is the one accepted external connection in Mode 3 (Deployment Model Spec §1.1); it delivers to the instance’s event receiver, a data plane component. The rule in §5 that the control plane never opens a connection into a tenant’s network concerns control-plane connections only and neither permits nor forbids this path. Every inbound source belongs to exactly one tenant, and the event layer stamps tenant_id on the event, as the Event Taxonomy Standard v1.0 requires of ingress. A delivery whose source cannot be resolved to one tenant is rejected at the edge.
The Hookdeck Event Gateway is a proprietary managed cloud service that neither Synedrix nor the customer deploys (Deployment Model Spec §2, accepted exception under ADR-OS-013), so a separate deployment per tenant cannot be required of it. The rule is met instead by dedicated configuration: every source, connection and destination in the gateway belongs to exactly one tenant and is never shared by two, and a delivery’s destination is that tenant’s own event receiver. This is the only data plane component isolated by configuration rather than by deployment; whether that is sufficient is tracked in §9.
The data plane is never pooled. A managed data plane serves one tenant. This rule has no exception for any component that Synedrix or the customer deploys; the vendor-operated Hookdeck Event Gateway above is isolated by dedicated per-tenant configuration instead of a separate deployment.
The plane boundary:
flowchart LR
subgraph cp["Control plane: pooled, tenant_id and row-level security on every tenant-owned table"]
CP["Registry, work graph, policy, approvals, audit index, entitlement, usage ledger, identity records"]
end
subgraph dp["Data plane: a separate deployment per tenant, never pooled"]
DP["n8n, LangGraph, Redis, Qdrant, Neo4j, QuestDB, Langfuse, PostHog, Grafana stack"]
EDGE["Egress edge: Hookdeck Outpost"]
DP --- EDGE
end
HK["Hookdeck Event Gateway: vendor-operated managed service, isolated by dedicated per-tenant configuration"]
HK -->|"delivers to the tenant event receiver"| DP
CL["Single-tenant control plane: same schema and row-level security, exactly one tenant"]
DP -->|"managed: initiates every connection, authenticated with tenant_id claim"| CP
DP -->|"self-operated: runs the control plane locally, in Mode 3 with no outbound connection to a pooled one"| CL
CP -.->|"never opens a connection into the data plane"| DP
4. Tenant-scoping rules for the control plane
These rules apply to every table in the control plane. The Agent DB Schema already requires row-level security for domain-pack and business-instance scoping; the tenant boundary is added outside it.
- Every table in the control plane carries a non-null
tenant_id. On a tenant-owned table it is the owning tenant. On a mixed table the reserved valueplatformmarks a shared reference row, and any other value is the owning tenant. This includesruntime.task_graphs,runtime.tasks,runtime.task_edgesandruntime.executions. - Row-level security is enabled and forced on every such table. Tenant access is checked by an
AS RESTRICTIVEpolicy, so its predicate is ANDed with all applicable role policies and cannot be widened by another permissive policy. The tenant policy grants no access by itself: each role must have a permissive policy for every command it may run, plus matching SQL privileges. Role policies enforce domain-pack and business-instance scope within the tenant boundary. Tenant-facing checks usecurrent_setting('app.tenant_id', true)and explicitly deny when the setting is absent or empty, including access to shared rows on mixed tables. Forcing matters because a table owner otherwise bypasses its own policies: tenant-facing services connect as a non-owner application role, and row-level security is forced even for the owner. A table without an applicable permissive policy denies all access. - The tenant for a request comes from the authenticated credential’s
tenant_idclaim. It is never taken from request input, a header the caller sets, or a body field. tenant_idis the leading column of every composite index and every cache, queue or namespace key derived from these tables.- No tenant-facing code path uses a role that bypasses row-level security. A bypass role exists only for platform operations and is never reachable from a tenant credential.
- A reference from one tenant-owned table to another is enforced by a composite foreign key,
(tenant_id, referenced_id)to a matching composite unique key(tenant_id, id)on the referenced table. Two separate single-column keys do not enforce this, because they can pair a child in tenant A with a parent in tenant B. The Agent DB Schema defines the concrete key names for each table. A reference to platform reference data (rule 8) is the one case where the parent’s scope differs from the child’s: the child carries an explicit scope column constrained to the reserved valueplatformor its owntenant_id, and its foreign key is(scope, id), so it still cannot reach another tenant’s row. Null-safe keys. Postgres does not check a composite foreign key when any referencing column is null, under its defaultMATCH SIMPLE.tenant_idis never null, butbusiness_instance_idmay be, so a key that includesbusiness_instance_idis enforced only for rows where that column is non-null. Such a key must be paired with aCHECKthat the child’sbusiness_instance_idis non-null whenever the reference column is set, and a tenant-level child, whosebusiness_instance_idis null by design (§2.4), references its parent through the separate(tenant_id, id)key instead.MATCH FULLon the three-column key would reject every tenant-level row, becausetenant_idis never null. A reference column that is optional by design is not a key column of this kind: when it is null there is no reference to check. - Cross-tenant access is never a scope setting. It happens only through a path that policy approves and that writes an audit record, as the memory fabric already requires for promotion.
- Platform reference data. Definitions shared by every tenant are not tenant-owned: the OS agent definitions
os-agt-001toos-agt-012and their versions, and the agent and workflow definitions that domain packs and Capability Modules contribute to the shared registry, which stay owned by the pack or module under the Agent Registry §6. These are read-only to tenant credentials, writable only by a platform or pack-publishing role, and listed by name in the Agent DB Schema. Tenant-owned, and so subject to rules 1 to 7, are tenant-specific controls, overrides and enable or disable settings, and the agents a Business Instance Extension contributes. Shared rows are marked in mixed tables by the reserved valueplatformin thetenant_idcolumn, as the Agent DB Schema defines.platformis never issued to a tenant, and a tenant-owned table never contains it. These tables therefore carrytenant_idlike every other, so the schema check in §8 applies to them without an exception. The Agent DB Schema’s Reference table class, whosetenant_idis “Absent”, is not used by any table and conflicts with rule 1; this spec governs, and §9 tracks removing the class from the schema. This rule is proposed against the ADR’s “every table” wording; see §9.
5. Cross-plane connectivity
A data plane may sit inside a customer’s private network, so the control plane never opens a connection into it.
- The data plane initiates every connection to the control plane. Work reaches the data plane over that outbound connection, by polling or by a stream the data plane opens, and never by inbound call.
- Every connection is authenticated, and the credential carries the tenant’s
tenant_idclaim. The mechanism belongs to the Identity and Access Spec. - Fail closed. While the control plane is unreachable, a data plane takes no action that has a side effect on a production system, whether it is a newly received request or work already in flight, which pauses at its next side-effecting step. Read-only work may continue, and the events it produces are held locally and delivered when the connection returns, so the audit trail stays complete. There is no in-flight exception, because the canon forbids writes without policy-aware orchestration visibility. Whether a data plane may cache policy to keep working is not decided; see §9. The local buffer is durable: it is written to storage that survives a restart of the data plane, so a restart during an outage loses nothing. An event is a past-tense fact, so it is recorded only after what it describes has happened. Before a side-effecting step is allowed, the data plane durably records an intent (not an event) in the same buffer; once the step succeeds, fails or is rejected, it persists the fact event with that outcome. A crash between the two leaves an intent without an outcome, which recovery resolves by emitting an outcome event, never by leaving a false record. Delivery is ordered per
correlation_id, in the order emitted. Re-sending a buffered envelope after reconnection is a delivery retry, not a re-emission: the envelope and itsevent_idare unchanged, which is what lets the receiver discard a duplicate byevent_idand create none. Emitting a new event for the same occurrence would be a re-emission, which the Event Taxonomy Standard forbids. If the buffer reaches its limit, work that would emit further events pauses, as side-effecting work already does. The limit itself is an open item (§9). - The Agent DB Schema also holds tenant-scoped content, such as
memory_recordsin Group G, in the same Postgres family. Which schema groups belong to the pooled control plane and which stay with the tenant’s data plane is not yet classified; see §9. Until it is, this spec makes no rule that the control plane holds no tenant content.
6. Variants and the deployment modes
| Variant | Data plane operator | Control plane | Modes |
|---|---|---|---|
| Self-operated | The customer | The same control-plane implementation, run locally as a single tenant | 1, 2 and 3 |
| Managed | Synedrix | Pooled and multi-tenant | 1 and 2 |
- The business instance declares
data_plane_operatorwith the valuecustomerorsynedrix. This is hosting configuration owned by the business instance, alongsidedeployment_mode. - A business instance declared
deployment_mode: air-gappedmust declaredata_plane_operator: customer. A configuration that does not is a validation error, enforced at business instance configuration time like the cloud-URI rule. - A managed business instance’s control plane is the pooled one. A self-operated business instance runs its control plane locally. A self-operated data plane using the pooled control plane is not defined by this version; see §9.
- The Deployment Model Spec §1.1 governs what each variant means for each mode. This section adds only the declaration and its validation.
6.1 The single-tenant control plane
A self-operated control plane runs the same schema, the same row-level security and the same code as the pooled one, with exactly one tenant. Row-level security stays enabled. There is no separate single-tenant code path, so the isolation behaviour is the one that is tested. In Mode 3 it makes no outbound connection to a pooled control plane.
7. Open-core placement
Under ADR-OS-006 §3.3, commercial code must not be required for a capability Principle 11 promises.
| Public core | Private synedrix-cloud class |
|---|---|
The control-plane schema, the tenant_id column and row-level security policies, tenant-scoped query paths, the single-tenant mode, the data_plane_operator declaration and its validation | Pooled operation of the control plane, tenant provisioning and lifecycle, billing, metering and entitlement enforcement for the managed offering |
8. Conformance
An implementation conforms when all of the following hold, and each is testable:
| Check | Expected result |
|---|---|
| A credential for tenant A queries any tenant-owned table for tenant B’s rows | Zero rows returned; no error that confirms the rows exist |
A tenant-facing role has no app.tenant_id setting, or it is empty, including when reading a mixed table’s shared rows | Access is denied |
| A role has a tenant policy but no applicable permissive policy and SQL privilege for a command | Access is denied |
A request carries a tenant_id in its body or a caller-set header that differs from the credential’s claim | The claim is used; the input is ignored |
| A tenant-owned table is created without row-level security or without a policy | Migration or schema check fails |
A control-plane table, including a mixed table, is created without a non-null tenant_id, or an index or foreign key omits it | Schema check fails |
A tenant credential writes a row whose tenant_id is platform, or a tenant-owned table contains platform | Rejected |
A business instance declares deployment_mode: air-gapped with data_plane_operator: synedrix | Configuration validation fails |
| A control plane is run single-tenant with row-level security disabled | Startup check fails |
| A tenant-facing service connects as a table owner, or a tenant-owned table does not force row-level security | Schema or startup check fails |
| A row in tenant A references a parent row in tenant B | Rejected by the composite foreign key |
| A Hookdeck source, connection or destination is configured for, or delivers to, more than one tenant | Configuration validation fails |
| The control plane attempts to open a connection into a tenant network | Not permitted by the deployment topology |
A data plane loses the control plane and then receives a request for any side-effecting action (an execute action, or any other write by any agent or workflow), or reaches a side-effecting step in work already in flight | The action does not proceed; read-only work continues and its events are delivered on reconnection |
A composite key, composite unique key or composite index on a control-plane table, or a cache, queue or namespace key derived from one, does not lead with tenant_id | Schema or code check fails. A single-column surrogate primary key is not covered, provided the table also has the composite (tenant_id, id) unique key that foreign keys reference |
| A tenant credential can assume, or reach, a role that bypasses row-level security or owns the table | Not possible; the deployment check fails |
| A cross-tenant access path is used | An audit record names both tenants, the actor and the policy decision that approved it |
| A managed data plane is configured to serve two tenants | Configuration validation fails |
A row whose key includes a null business_instance_id references a parent in another tenant | Rejected, by the non-null CHECK on the instance-scoped key or by the (tenant_id, id) key |
| A data plane restarts during a control-plane outage after holding events | After reconnection every held event is delivered in per-flow order, none is lost, and the receiver accepts no duplicate event_id; a step recorded as an intent with no outcome gets an outcome event |
9. Open items
- Platform reference data (§4.8). ADR-OS-008 requires
tenant_idand row-level security on every control-plane table. Shared definitions carry the reserved valueplatformin that column, as the Agent DB Schema defines, so they do carry one. The ADR’s owner should confirm that reading of “every table”. - Agent DB Schema Reference class (§4.1, §4.8). The schema’s table classes still list a Reference class with an absent
tenant_id, while its own rules state that every table carries a non-nulltenant_id. No table uses the class. It should be removed in the next schema revision so a conforming Reference table cannot fail the §8 check. - Tenancy of the Hookdeck Event Gateway (§1, §3). The inbound gateway is vendor-operated infrastructure that Synedrix does not deploy per tenant. This spec requires dedicated per-tenant sources, connections and destinations. Whether that is enough, or each tenant needs its own Hookdeck project or organisation, and how an inbound source is mapped to a tenant, is not decided.
- Tenant identifier on the event envelope (§3). Resolved by the Event Taxonomy Standard v1.0, which adds a required
tenant_idstamped by the event layer. What remains is the implementation: the event receiver and the gateway configuration that resolve an inbound source to its tenant. - Control-plane versus data-plane tables (§3, §5.4). The Agent DB Schema keeps
memory_recordsand other tenant-scoped content in Supabase alongside registry and runtime tables. The schema groups need classifying as pooled control-plane tables or per-tenant data-plane tables before any rule about where content may live can be made. The Agent DB Schema v1.0 classifies every table and makes each safe to pool, but leaves the hosting of the content-bearing groups open. - Outage buffer limit (§5.3). How large the durable local buffer may grow, how long events may be held, and what the operator is told when it fills are not set.
- Policy caching during a control-plane outage (§5.3). Failing closed is the safe default and may be too strict for a managed data plane that briefly loses its connection. Whether and how policy may be cached is undecided.
- Self-operated data plane with the pooled control plane (§6.3). A customer who owns the data plane but wants a managed control plane is not defined here.
- Identifier format.
tenant_idis specified as opaque, immutable and never reused. A format, and its relationship to the identifiers in the Naming Convention Spec, are not set. - Dispatch mechanism (§5.1). Polling versus a data-plane-initiated stream is not chosen.
- Moving a business instance between tenants. Forbidden by §2.1. Whether a controlled transfer is ever needed is not decided.
- Pooled control-plane limits. Whether very large tenants move to a dedicated schema or database is not decided here, as in the ADR.
Versioning
- Version: 0.4
- Supersedes: v0.3
- Related: ADR-OS-008 — Tenancy Model, Synedrix OS - Deployment Model Spec, Synedrix OS - Core Principles, Synedrix OS - Agent DB Schema
Related Documents
Core Principles and Canonical Reference
- Synedrix OS - Core Principles — Principle 11 constrains how tenancy may be implemented in each deployment mode.
- Synedrix OS - Consolidated Canonical Reference — Consolidated synthesis; this spec is indexed there as row 25 (Draft).
Decisions
- ADR-OS-008 — Tenancy Model — The decision this document implements.
- ADR-OS-006 — Repository Topology and Open-Core Boundary — §3.3 and §3.5 set the open-core placement in §7.
Specifications this document depends on
- Synedrix OS - Deployment Model Spec — §1.1 defines the hosting variants this document declares.
- Synedrix OS - Agent DB Schema — Owns the table definitions and the row-level security requirement this document extends.
- Synedrix OS — Memory Fabric Spec — The existing
tenant_idscope rule. - Synedrix OS — Goal Model Spec and Synedrix OS — Task Graph Spec — The
business_instance_idscoping and theruntime.*schema. - Synedrix OS - Responsibility Matrix — Tier ownership; tenancy rows are added separately.
- Synedrix OS — Naming Convention Spec — Identifier conventions the
tenant_idformat would follow.
Synedrix OS · October 2026