Skip to main content

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.
Tenant resolution, then, cannot happen per-request on these surfaces — it happens per-event, from the event payload. Any table that joins the public catalog exists to support exactly this per-event resolution, plus the small number of platform-wide read surfaces described in Section 7.

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.
When the assignment misses (for example, a DID your team ported in directly from a carrier), the resolver drops to the reverse map below.

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:
Writes happen when a number is purchased, ported, or released; the hourly reconcile (Section 6) converges any drift.

dnis_routes — pattern routing over the pool

One row per dialed-number pattern, so hundreds of DIDs can funnel into one queue:
The resolver honours a matched row only after scoping it to the org that owns the dialed number; an exact per-DID row in 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.

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):
  1. Inbound voice: dialed number → owning org, then termination target (inbound_routes, then dnis_routes).
  2. Inbound MO SMS and carrier DLRs: destination DID → owning org via platform_numbers / its assignment, then phone_number_owners, then the bounded fallback scan.
  3. Edge admission checks: platform_ip_denylist and honeypot_dids.
  4. Your DID inventory and purchase flow: the platform pool rows behind your Numbers page.
  5. The tenant-facing read surfaces in Section 7.
Reads served from your tenant schema. Everything you create or receive as an org: 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:
A single published item is a public fetch by id; draft and pending-review items never appear on the unauthenticated surface. Your number inventory. The available-Numbers browse that backs your purchase flow joins the catalog’s pool rows:
Both examples are reads. There is no tenant-facing write path into the catalog: purchases, ports, and releases go through platform write paths that backfill the catalog rows, each attributed to exactly one org.

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-map reconcile walks every tenant’s numbers and converges phone_number_owners drift; 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.
There is no snapshot isolation between schemas, and the catalog is not a replica of tenant data: it holds only the catalog rows above, written directly. The one consistency guarantee that matters operationally is the released-DID invariant — a released number’s reverse-map row is deleted in the same write that releases it, so a recycled DID never routes inbound traffic to its former owner.

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.
Operators contribute to the catalog indirectly: the DNIS and per-DID routing you configure is stored in the catalog because the edge needs it before tenancy exists, but every read of it is re-scoped to your org (Section 4).

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:
  1. 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.
  2. 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.
  3. 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.
  4. 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.
The drift-convergence mechanism behind tests 2–3 is the hourly 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.
The pair of read guards and write guards is enforced in code; the page’s job is to make sure adding to the set is never an accident.