> ## 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.

# Inbound voice routing: why the tables are shared

> The shared-catalog design decision behind inbound voice routing — why `dnis_routes` and `inbound_routes` live outside tenant schemas, and the row-level organization discipline that enforces tenant safety anyway.

# Inbound voice routing: why the tables are shared

Most of your data in Orbit lives in one PostgreSQL schema per tenant — messages, contacts, recordings, and flows never appear in any other tenant's schema ([Tenant isolation](/concepts/tenant-isolation) lays out that split). Inbound voice routing breaks that pattern on purpose: its two tables, `dnis_routes` and `inbound_routes`, live in the shared platform catalog. This page is the design decision behind that exception — why the tables are shared, and what enforces the tenant boundary anyway when schema separation is deliberately absent.

## What is actually shared

Exactly two routing tables sit in the shared catalog, and both carry an organization id on every row:

| Shared table     | What a row says                                                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `dnis_routes`    | "Dialed numbers matching this pattern (E.164 prefix or regex) resolve to this route" — a class of numbers, not one number              |
| `inbound_routes` | "Calls to this one number terminate this way" — one DID, one termination type plus its config, optional fallback, optional SMS handler |

Shared here means *one physical table in the platform catalog that every tenant's rows live in* — not that rows become visible to other tenants. Every routing row belongs to exactly one organization because each carries its owning organization's id.

## The one problem a shared table solves: resolution before tenancy exists

An inbound call arrives at the SIP edge as a SIP INVITE addressed to a dialed number. In that moment:

* The dialed number is the **only** input — there is no API key, no session, no authenticated caller on the platform side yet.
* The edge must produce a routing decision at call setup, before media flows, or the caller lands on dead air.
* A dialed number is a global identifier: `+18005550100` is unique across the whole platform, not scoped to a tenant.

Per-tenant read paths authenticate first and then pin a tenant schema for the query. That works for the dashboard and the API, where a credential exists before any query runs. The SIP edge has no such moment: tenancy does not exist until the dialed number tells it which organization the call belongs to. A per-tenant routing table would answer that question only *after* the edge already knew the answer — a circular lookup that cannot resolve.

So the tables that answer "which organization owns this number" and "where do calls to this number terminate" must live in one catalog reachable without a tenant context. Routing data is in the shared schema for the same reason the `organizations` table is: it is part of the lookup that establishes tenancy, not part of tenant data.

## How the decision gets made

Once the catalog layout settled the *where*, the remaining design points are about *how* a resolver behaves across tenant lines it can physically touch:

* **Org-first resolution.** A DNIS match is only ever evaluated within the organization that currently owns the dialed number. The resolver asks which organization the number belongs to, then reads **that organization's** pattern rules and per-number rows. Your `+1800` prefix rule can never catch another tenant's toll-free DID, and that tenant's rules are never candidates for your number — even when a regex would match. Tenant precedence to a dialed number is decided by ownership, never by rule generality.
* **The stale-route guard.** The per-number route row and the number-ownership record are read separately, and the route is honored only when its organization agrees with the current owner. A released-then-rebought DID can leave a stale route behind from the prior owner; on organization mismatch the route is rejected on the spot and the call re-resolves through the remaining chain (pattern rules, org default, safe default). The shared table physically holds both tenants' eras of the same number; the guard is what keeps routing obedient to the current owner only.
* **Ownership is re-validated per call, not cached across calls.** The ownership read runs at every call setup, so a number that changes hands starts resolving to the new owner from the first call after the change — no stale-attribution window.
* **Suspended and deleted organizations are rejected at the same gate.** A resolved organization that is soft-deleted, cancelled, or suspended fails the resolution and the call takes the safe-default path — a shared catalog never routes into a frozen tenant.
* **The fail-open floor is the safe default.** When the resolution database is briefly unreachable, the call degrades to a polite "line not available" announcement, never to a dropped INVITE. For inbound routing, **safe default + org-first + per-call stale validation** is the full answer to "shared but safe anyway."

The mechanics of the chain — verb plans, caller-id filters, business hours, the four-step ordering — live in [Inbound voice routing](/concepts/inbound-voice-routing); this page is only about where the tables sit and why that is safe.

## What this costs, and what stays per-tenant anyway

The trade accepted by putting routing rows in one catalog:

* **Tenant discipline moves from the schema layer to the row layer.** Nothing structurally prevents a cross-tenant read; the org-first rule, the ownership-consistency guard, and per-call re-validation carry the whole boundary. The [Tenant isolation](/concepts/tenant-isolation) page lists both tables explicitly in its shared-catalog section so the exception is auditable rather than implicit.
* **Reads at the SIP edge are unscoped by design, and that property is exercised on every call** — a regression in the shared-catalog resolver is visible in call behavior, not only in a permission check.
* **Aggregation stops at the route-config cell.** Only the routing decision (target type plus target config) lives in the shared catalog. Once the decision resolves to an organization, the *content* of the call — conversation record, recording, voicemail, queue interaction — is written into that tenant's schema in the ordinary way, so live-call data never sits in the shared tables.

What deliberately was **not** done: colocating routing tables in per-tenant schemas behind an edge-side schema-selection step. That alternative adds latency to every call setup and still fails the circular-lookup problem at number-acquisition time; the row-level guards were preferred over schema placement.

Work through the walkthrough in [Inbound number routing](/guides/inbound-number-routing) for the operator-facing side of per-number and DNIS routes; the SMS analogue of this same shared-resolution problem lives in [Inbound message resolution](/concepts/inbound-message-resolution).

## See also

* [Inbound voice routing](/concepts/inbound-voice-routing) — the resolution mechanics this page's tables feed
* [Tenant isolation](/concepts/tenant-isolation) — the shared-catalog section that lists both routing tables as the deliberate exception
* [Inbound message resolution](/concepts/inbound-message-resolution) — the SMS/MO ownership layer answering the same edge-resolution problem
* [Voice call lifecycle](/concepts/voice-call-lifecycle) — the state transitions that follow the routing decision
* [Provisioning and test mode](/concepts/provisioning-and-test-mode) — the readiness window during which routing reads only the shared catalog
* [Jambonz softswitch model](/concepts/jambonz-softswitch) — the SIP edge where the unscoped routing read executes
