Tenant ownership and the information flow
Table of Contents
1. Summary
Every row belongs to one of three owners, and the schema names only two. A tenant owns its own business data; the system tenant owns the installation's registers and the shared datasets every tenant reads; the installation owns the facts about the deployment itself — which services run, which nodes report, what the bus is doing. Because the third owner has no name, its rows are stored under the system tenant and read through policies written for tenant data, so every read of them is a workaround. This page names the three owners, states the mechanism each deserves, and lists where the missing third class already bites.
2. Detail
2.1. The three owners
| Owner | What it owns | Examples |
|---|---|---|
| A tenant | Its own business data | parties, portfolios, books, trades, workunits, batches, results, a party's quote series, routes and subscriptions, synthetic configuration |
| The system tenant | The installation's registers, and the datasets every tenant reads | the tenants registry, service accounts, permissions, roles, seed profiles; countries, currencies, coding schemes, asset classes, avatars, calendars |
| The installation | Facts about the deployment itself, owned by nobody in particular | the services roster and its instances, hosts and wrapper nodes, grid and node samples, telemetry logs, message-bus samples, the database and build row, the expected-services registry, the tick streams and their points |
The third row is the one the schema does not name. Its rows have no tenant in any meaningful sense: a node sample describes a machine, and the machine runs whatever work the installation gives it.
A tick point is the same kind of row: an observation of a source, not a tenant's data. It carries no tenant and no row-level security, only the marketdata service's roles reach it, and a party reads it through its own series and entitlement. See A source tick is stored once and entitled when it is read.
2.2. What the schema does today
Measured across projects/ores.sql/create on 2026-10-07, by a throwaway
script that matched every create policy statement and classified its body
by the tenant functions it names: a widening compares the session to the
system tenant, a system-owned policy compares tenant_id to the system
tenant, an own-tenant policy names the current tenant and no system
function, and a policy that names neither is scoped by party, sandbox or
role. The same run reproduced the guard's count of
check_rls_system_tenant_widenings.py:
| Policy shape | Count | Meaning |
|---|---|---|
| Own tenant only | 353 | tenant_id = ores_iam_current_tenant_id_fn() |
| Party or sandbox scoping | 152 | keyed on party_id or a sandbox, with no tenant predicate |
| A system-tenant session sees every tenant | 10 | current = system; every one is listed as under audit by the guard |
| System-owned rows readable by every tenant | 14 | tenant_id = system; the shared-reference idiom |
| A role-scoped cross-tenant read | 3 | using (true) granted to the market-data service role, keyed on neither tenant nor party |
| Total | 532 | every policy in projects/ores.sql/create |
The count does not include the policy on accounts that limits a read to the parties the caller's account works in. That policy is a special case and belongs to accounts alone. It is hand-written, it names no system tenant function, and it only narrows a read the own-tenant policy already bounds. See Multi-Tenancy Architecture, "The accounts exception".
The guarded shape is the third. check_rls_system_tenant_widenings.py
fails any new one, and its message is the rule: "A service that needs a
tenant's data acts inside that tenant, with a token for it (Token Exchange),
instead."
2.3. The four mechanisms, and what each is for
| Mechanism | What it does | Correct use |
|---|---|---|
| Own-tenant policy | Scopes every read to the session's tenant. | A tenant's data. |
System-owned policy (tenant_id = system) |
Every session sees the system's rows. | Shared reference data. |
:tenant_read_scope: shared |
The read states no tenant predicate; the policy decides. | A read whose rows the policy already admits. |
tenant_context::with_tenant, or internal impersonation |
Acts inside a tenant. | Reaching a tenant's data from outside it. |
Data read across tenants states the building blocks of the second and third rows together, and the checklist for a new shared table.
The last row has two forms because it serves two layers: with_tenant sets
app.current_tenant_id for SQL, and
Internal Actor Impersonation
mints a JWT so a service can drive another tenant's NATS handlers. Both are
the Token Exchange pattern.
2.4. The hole
Installation-scoped data is stored in tenant-scoped tables. Every read of it must therefore answer "which tenant?", and there is no right answer — only four workarounds: write it under the system tenant and hope the policy admits it; widen the policy, which the guard exists to stop; state no predicate and let a policy written for tenant data decide; or loop over tenants and impersonate each.
2.5. Where it bites today
- The grid summary counts the wrong thing.
ores_compute_grid_stats_fntakes a tenant, and the poller that calls it holds the system tenant, so the stored summary is that one tenant's slice. Measured on the running fleet: the stored sample reportedtotal_workunits = 0while the installation held one workunit and two batches under another tenant. - The node and grid samples carry a constant, not attribution. Every
sample is written under the system tenant, because the launcher bakes the
system tenant into every wrapper's
--tenant-id. Thetenant_idon those rows says nothing about the node or the work; it holds the same value in every row. - The roster works by accident. The services roster reads every expected instance and every instance's last report. It works only because every process that reports is a service account, and service accounts live in the system tenant — so no policy had to be widened. The same read over a tenant-owned subject would not work.
- The compute service cannot enumerate tenants. The tenants registry is
owned by the system tenant, so a system-tenant session may read it — but
the only C++ that reads that table is
ores.iam.core, andores.compute.servicehas no dependency on it. The one cross-tenant read the design blesses is unreachable from a component that needs it.
2.6. The design
Name the third owner, and give it the mechanism it has earned.
- Class I — the installation. Rows that describe the deployment. Services
write them, and they carry the system tenant as a constant rather than
as an owner. Their policy is the shared-reference shape —
tenant_id = current or tenant_id = system— which the guard does not flag, because it admits no tenant's data to anyone: the rows it exposes are the installation's own. The permission decides who may call the read, exactly ascompute::grid_samples:readdoes today. - Class R — shared reference. Already right: system-owned rows every tenant reads, with writes tenant-scoped.
- Class T — a tenant. Already right: an own-tenant policy, with cross-tenant access only by acting inside the tenant.
A cross-tenant aggregate is Class T, not Class I. A count of every tenant's workunits is not a fact about the installation; it is a fact about several tenants at once, and no single tenant owns it. The sanctioned way to compute one is to act inside each tenant. If the Operations area needs installation-wide work totals, the aggregate needs an owner allowed to enter tenants — the mechanism Read each tenant's cache partition inside its tenant is building, and the decision belongs with Audit the policies that let the system tenant read every tenant.
The grid's work totals are the first such aggregate. The poller enters each tenant, reads that tenant's counters there, and writes the sum to
ores_compute_work_totals_tbl, whose policy admits a session of the system tenant and no other. The sum is stored in a table of its own and not in the fleet row, because every session reads the fleet row. A function that runs with its owner's rights to count across tenants is not the way: it lets any database role read any tenant's counts.
2.7. What follows for the grid
The fleet is Class I: hosts, wrappers, node samples and the services roster describe the installation, and every session the permission admits should read them whole. The work counts are Class T: workunits, batches and results belong to the tenant that owns them.
The installation-wide work totals are now written by the poller into a table that only the system tenant reads (entry E8 of the RLS and permission exceptions register).
So the two questions the screen raises — "is the grid healthy?" and "what is my tenant running?" — are the same problem seen from two sides, and both want the same answer: read the fleet as the installation, and enter a tenant for its work. That makes the Operations grid a read-only screen over the fleet, and moves installation-wide work totals to the audit, where the cross-tenant mechanism is being decided.
The usage tab applies this. The compute service counts one tenant and groups it by party. The BFF enters each tenant in turn and reads that count there, so the operator's breakdown by tenant needs no policy that reaches across tenants. The page Data read across tenants states the rule and what an exception needs.
3. See also
- Token Exchange — the pattern both forms of "act inside a tenant" apply.
- Internal Actor Impersonation — the handler-layer form.
- Read another tenant's data from inside it — the memory that states the rule.
- Audit the policies that let the system tenant read every tenant — where the existing widenings are being removed.
- Read each tenant's cache partition inside its tenant — the first cross-tenant read rebuilt on token exchange.
- Watch the compute grid — the screen whose counters this page explains.
- The grid read is scoped to the session tenant — the capture that started this study.