> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The public catalog: a closed set of shared tables

> Which tables live in Orbit's shared public schema, why the edge resolves tenant from them, and the checklist for adding a new public table.

# 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 lists the closed set, links each table's owning design page, and gives the engineering checklist for adding a new one. [Tenant isolation](/concepts/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, and it is joined by nothing else.

## Section 2 — The closed set

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):

| Public table                                       | What it holds                                                                        |
| -------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `organizations`                                    | Orgs (id, tenant\_id, plan, provisioning flag) — the lookup that establishes tenancy |
| `dnis_routes`                                      | Shared DNIS pattern → org routing                                                    |
| `inbound_routes`                                   | Shared inbound-call termination targets                                              |
| `phone_number_owners`                              | Cross-tenant reverse map: number → owning org                                        |
| `honeypot_dids`                                    | Honeypot DID catalog for spam-signal detection                                       |
| `platform_ip_denylist`                             | Platform-wide IP denylist                                                            |
| `platform_numbers` / `platform_number_assignments` | The platform-wide DID pool and each assignment                                       |
| `sip_devices` / `tenant_sip_credentials`           | Shared SIP device + per-tenant credential index                                      |

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.

## 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](/concepts/inbound-voice-routing-shared-catalog) — `dnis_routes` and `inbound_routes`: resolution before tenancy exists, and the org-first + stale-route guards.
* [Inbound message resolution](/concepts/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](/concepts/tenant-isolation) — `organizations` and the catalog-level ownership guards that apply to every public table on read and on write.

## Section 4 — 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](/concepts/tenant-isolation#section-4-3)) 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 5 — 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](/concepts/tenant-isolation) 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.
