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

TermMeaning
TenantThe contracting organisation. The unit of isolation, entitlement and metering
Business instanceThe configured deployment for a company, team or business unit, per the Taxonomy & Platform Spec. Belongs to exactly one tenant
Control planeThe shared coordination services: agent registry, work graph, policy, approvals, audit index, entitlement, usage ledger and identity records
Data planeThe 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_idThe outer scope key. Opaque, immutable once issued, and never reused
business_instance_idThe inner scope key. Unique within a tenant
Data plane operatorThe party that runs the data plane: the customer (self-operated) or Synedrix (managed)

2. Hierarchy

  1. A tenant owns one or more business instances. A business instance belongs to exactly one tenant and cannot be moved between tenants.
  2. tenant_id is the outer key in every scope tuple. business_instance_id is the inner key and is meaningful only together with its tenant_id. The memory fabric’s rule that tenant_id is mandatory and always first is unchanged.
  3. 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.
  4. A record that belongs to a tenant but to no single business instance carries tenant_id and a null business_instance_id. A record never carries a business_instance_id without its tenant_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

PlaneHoldsIsolationNever
Control planeRegistry, work graph (runtime.*), policy, approvals, audit index, entitlement, usage ledger, identity recordstenant_id and row-level security on every tenant-owned tableIs shared by two tenants without tenant_id scoping on every tenant-owned table
Data planeThe components named in §1A separate deployment per tenantIs 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.

  1. 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 value platform marks a shared reference row, and any other value is the owning tenant. This includes runtime.task_graphs, runtime.tasks, runtime.task_edges and runtime.executions.
  2. Row-level security is enabled and forced on every such table. Tenant access is checked by an AS RESTRICTIVE policy, 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 use current_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.
  3. The tenant for a request comes from the authenticated credential’s tenant_id claim. It is never taken from request input, a header the caller sets, or a body field.
  4. tenant_id is the leading column of every composite index and every cache, queue or namespace key derived from these tables.
  5. 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.
  6. 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 value platform or its own tenant_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 default MATCH SIMPLE. tenant_id is never null, but business_instance_id may be, so a key that includes business_instance_id is enforced only for rows where that column is non-null. Such a key must be paired with a CHECK that the child’s business_instance_id is non-null whenever the reference column is set, and a tenant-level child, whose business_instance_id is null by design (§2.4), references its parent through the separate (tenant_id, id) key instead. MATCH FULL on the three-column key would reject every tenant-level row, because tenant_id is 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.
  7. 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.
  8. Platform reference data. Definitions shared by every tenant are not tenant-owned: the OS agent definitions os-agt-001 to os-agt-012 and 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 value platform in the tenant_id column, as the Agent DB Schema defines. platform is never issued to a tenant, and a tenant-owned table never contains it. These tables therefore carry tenant_id like every other, so the schema check in §8 applies to them without an exception. The Agent DB Schema’s Reference table class, whose tenant_id is “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.

  1. 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.
  2. Every connection is authenticated, and the credential carries the tenant’s tenant_id claim. The mechanism belongs to the Identity and Access Spec.
  3. 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 its event_id are unchanged, which is what lets the receiver discard a duplicate by event_id and 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).
  4. The Agent DB Schema also holds tenant-scoped content, such as memory_records in 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

VariantData plane operatorControl planeModes
Self-operatedThe customerThe same control-plane implementation, run locally as a single tenant1, 2 and 3
ManagedSynedrixPooled and multi-tenant1 and 2
  1. The business instance declares data_plane_operator with the value customer or synedrix. This is hosting configuration owned by the business instance, alongside deployment_mode.
  2. A business instance declared deployment_mode: air-gapped must declare data_plane_operator: customer. A configuration that does not is a validation error, enforced at business instance configuration time like the cloud-URI rule.
  3. 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.
  4. 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 corePrivate 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 validationPooled 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:

CheckExpected result
A credential for tenant A queries any tenant-owned table for tenant B’s rowsZero 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 rowsAccess is denied
A role has a tenant policy but no applicable permissive policy and SQL privilege for a commandAccess is denied
A request carries a tenant_id in its body or a caller-set header that differs from the credential’s claimThe claim is used; the input is ignored
A tenant-owned table is created without row-level security or without a policyMigration 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 itSchema check fails
A tenant credential writes a row whose tenant_id is platform, or a tenant-owned table contains platformRejected
A business instance declares deployment_mode: air-gapped with data_plane_operator: synedrixConfiguration validation fails
A control plane is run single-tenant with row-level security disabledStartup check fails
A tenant-facing service connects as a table owner, or a tenant-owned table does not force row-level securitySchema or startup check fails
A row in tenant A references a parent row in tenant BRejected by the composite foreign key
A Hookdeck source, connection or destination is configured for, or delivers to, more than one tenantConfiguration validation fails
The control plane attempts to open a connection into a tenant networkNot 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 flightThe 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_idSchema 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 tableNot possible; the deployment check fails
A cross-tenant access path is usedAn audit record names both tenants, the actor and the policy decision that approved it
A managed data plane is configured to serve two tenantsConfiguration validation fails
A row whose key includes a null business_instance_id references a parent in another tenantRejected, 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 eventsAfter 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_id and row-level security on every control-plane table. Shared definitions carry the reserved value platform in 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-null tenant_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_id stamped 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_records and 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_id is 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


Core Principles and Canonical Reference

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 · October 2026