The public catalog: a closed set of shared tables
Orbit resolves tenancy two ways: inbound, from an API key or session; and the reverse, from the destination of an inbound event. The reverse direction needs a small set of tables every tenant and every edge node can read without a tenant context — the public catalog. This page is its hub: it names the closed set, shows what a row in each table looks like, separates the reads that hit the catalog from the reads that hit your tenant schema, and gives the engineering checklist for adding a new one. Tenant isolation documents the schema-per-tenant split; this page documents the deliberate exception to it.Section 1 — What reaches the edge unauthenticated
Three inbound surfaces reach Orbit before any tenant context exists:- Voice. A SIP INVITE addressed to a dialed number. The dialed number is the only input — there is no API key, no caller credential, no session. The edge must decide where the call terminates before media flows.
- Mobile-originated SMS. An inbound SMS carries a destination DID and a sender. The resolver must answer “which tenant owns this number” before routing rules or inbox assignment run.
- Delivery receipts. A carrier DLR arrives with no bearer token. Like MO SMS, it resolves tenant from the destination number alone.
Section 2 — The closed set, and what a row looks like
The public schema holds catalog and routing rows — one row per platform artifact or per org — never per-tenant working data. The full set, closed at eight tables (the platform DID pool and assignments count as one pair):
The shapes below name the fields the resolution chain relies on. They are illustrative payloads, not a wire contract: exact columns beyond these live in your SDK’s types and in each table’s owning page (Section 3).
organizations — the tenancy anchor
One row per org. Your API key resolves to one of these rows, and every catalog row your traffic touches traces back to it.
tenant_id names the tenant schema (tenant_a1b2c3d4) that holds the org’s working data; tenant_schema_ready is the provisioning flag Section 7’s setup path depends on.
platform_numbers + platform_number_assignments — the DID pool
One row per platform number, plus one assignment row whenever it is attached to an org. The pair answers “who owns the destination of this inbound event?” before any tenant context exists.
phone_number_owners — the O(1) reverse map
One row per routable DID, keyed by the number itself so the inbound lookup is a hash lookup, not a scan:
dnis_routes — pattern routing over the pool
One row per dialed-number pattern, so hundreds of DIDs can funnel into one queue:
inbound_routes always wins over a pattern.
inbound_routes — the per-DID termination target
One row per DID, carrying where the call terminates (queue, agent, IVR, or SIP forward).
honeypot_dids — the spam-signal catalog
One row per trap number the platform publishes; traffic addressed to one is a spam signal rather than a routable event. Rows carry the trap category and the org context the signal is attributed to.
platform_ip_denylist — the platform-wide block list
One row per blocked source address (or range) with an expiry, applied to inbound surfaces before tenant resolution.
sip_devices + tenant_sip_credentials — the device index
One row per registered SIP device, plus the per-tenant credential index the edge uses to authenticate a device back to its org.
This set is closed. Nothing else joins it unless a concept page documents the reason — the same ownership discipline this page applies. If you are reading this to answer “is a public table allowed?”, the answer is only yes when a concept page exists for the exception.
Platform-wide operational tables (auth sessions, marketplace listings, published pricing) also live in the public schema under their own ownership pages; they are not part of the inbound-resolution catalog this page governs, and adding one follows its own documented exception rather than the checklist below.
Section 3 — Where each owning concept sits
Each table or pair above has one owning design page. These pages own the rationale; this page fetches, it does not repeat.- Inbound voice routing: why the tables are shared —
dnis_routesandinbound_routes: resolution before tenancy exists, and the org-first + stale-route guards. - Inbound message resolution —
phone_number_owners: the O(1) reverse map that resolves MO SMS and DLR events to one tenant, plus the bounded fallback scan. - Tenant isolation —
organizationsand the catalog-level ownership guards that apply to every public table on read and on write.
Section 4 — Catalog reads vs. tenant-schema reads
The boundary is one rule: the catalog answers “who owns this?”; your tenant schema answers “what does this org have?” Every read in Orbit lands on exactly one side. Reads served from the public catalog. These run with no tenant context at all (the first three), or with the org’s own key (the rest):- Inbound voice: dialed number → owning org, then termination target (
inbound_routes, thendnis_routes). - Inbound MO SMS and carrier DLRs: destination DID → owning org via
platform_numbers/ its assignment, thenphone_number_owners, then the bounded fallback scan. - Edge admission checks:
platform_ip_denylistandhoneypot_dids. - Your DID inventory and purchase flow: the platform pool rows behind your Numbers page.
- The tenant-facing read surfaces in Section 7.
contacts, messages, campaigns, sender_pools, flows, agents, recordings, inbox conversations, CDP and usage events (Tenant isolation Section 2).
Two consequences follow from the split:
- A catalog row never contains another tenant’s working data. Catalog rows carry ownership columns (
org_id,assigned_to_org_id,organization_id), and every read re-checks them, so a cross-tenant read is a guard failure, not a lucky miss. - A pool id or any other per-tenant resource id is only unique within one tenant schema. The catalog is where platform-wide uniqueness lives; your schema is where your org’s resources live.
Section 5 — The tenant-facing read surfaces
Three product surfaces read catalog rows on your behalf. None of them exposes another org’s tenant data. Compliance posture. Your compliance posture is assembled from your own configuration, records, and audit history, read from your tenant schema. The catalog’s role is the tenancy anchor that scopes that read: the posture plane never queries another org’s schema, and an auditor following your posture guide crosses no tenant boundary. See Posture overview and the tenancy and posture audit map. Marketplace listings. Published agent templates and integrations are platform-wide rows, readable without authentication once published:Section 6 — Freshness: where the catalog can lag, and by how much
Most catalog rows are written synchronously with the event that creates them: a number purchase flips its assignment in the same transaction, and a release deletes its reverse-map row in the same write. For those, the catalog is as current as the tenant schema. Two writes are deliberately deferred, and both are bounded:- Reverse-map backfill. The hourly
inbound-reverse-mapreconcile walks every tenant’s numbers and convergesphone_number_ownersdrift; the operator CLI runs the same backfill for one tenant or as a dry run. Steady-state resolution never depends on it — the bounded fallback scan covers the mid-window gap — so a number that is brand new or just released still routes correctly while its row converges. - Schema migrations. New columns on catalog tables roll out on the same per-ledger, drift-repairing mechanism as tenant schemas (Tenant isolation Section 2.1). Reads never block on a pending migration: every migration body is idempotent and guarded, so a catalog read during a rollout sees the pre-rollout column set, never a partial one.
Section 7 — When you actually touch this surface
As a tenant, you reach the catalog through three routines:- Posture audits. When your compliance team walks a posture guide, the evidence they read is org-scoped by the catalog’s ownership columns; the audit-ledger posture rollup documents what an auditor sees.
- Marketplace installs. Browsing and installing a published template reads platform rows (Section 5); nothing about the install writes into another org’s schema.
- Number purchases, ports, and releases. Each one backfills or deletes catalog rows through the platform write path. If a recycled DID ever appears to receive another org’s traffic, that is the released-DID invariant in Section 6 failing, and it is a platform-side incident, not something a tenant setting controls.
Section 8 — The four tests a new public-schema table must pass
When you consider adding a table to the public schema, every one of these must hold:- It resolves tenant from payload alone. The edge has no credential context, so the row must carry enough by itself to pin the owning org. If resolution needs anything outside the payload, the table does not belong here.
- Resolution is O(1) per event. A public-catalog read is on the inbound hot path. Bounded fallback scans exist to cover drift, not as the steady state — a new row must index its lookup key, not contribute to a scan.
- Ownership is re-validated per event. Never cached, never “owned forever.” A recycled DID changes hands; the guard that re-checks ownership on every event is what keeps the new owner’s inbound from landing on the previous owner’s endpoint.
- Writes are tenant-bounded. A purchase, admin action, or reconcile backfills rows; it never writes on behalf of no-owner. If a row cannot be attributed to exactly one org at write time, it must not be written.
inbound-reverse-map reconcile (reverse-map section) plus the operator CLI that runs the same backfill for one tenant or as a dry run. Any new public table that resolves inbound traffic must have an equivalent reconcile before it can go live.
Section 9 — Checklist: adding a public table
Run this before touching the schema, and link the three artifacts it names in your change description:- A concept page documents the design rationale, and the table is added to the closed set above — same commit.
- The read guard is in place: an index on the lookup key, ownership re-checked per event, ambiguity dropped rather than guessed.
- The write guard is in place: writes are tenant-bounded, and idempotent so the reconcile can converge any drift.
- A reconcile scheduler and a CLI backfill exist and are wired to converge the reverse-map-like index the table maintains.
- Tenant isolation Section 1 lists the new table, and this page was updated — because the set is closed, adding to it is a documented decision, not a migration side effect.