Deployment modes

Fully managed cloud vs managed numbers with a self-hosted gateway — who provides the brain, and where the media runs.

Every deployment answers two questions: who provides the brain (the LLM), and where does the media run (the gateway that carries call audio). OpenPhonex's two MVP launch paths differ only on the second.

OpenPhonex deployment modes: fully managed cloud versus managed numbers with a self-hosted gateway

fully_managed_cloud

OpenPhonex Cloud runs everything: the API, the hosted media gateway, the carrier/provider account, numbers, credits, the compliance workflow, and abuse controls. You bring an agent and a scoped key; the phone side is entirely managed. This is the fastest path to a live line.

Nothing here is provisioned per workspace. One hosted dialer places every managed outbound call and one hosted runner drives every real-time Simulations Lab session, for every workspace, and both read a job's workspace, project, agent, caller number and destination from the call record itself. There is no worker to run, no worker key to install and no gateway deployment to register before POST /v1/calls works. What outbound still depends on is your outbound authorization, not any setup of your own.

managed_numbers_self_hosted_gateway

You run the media gateway yourself (Docker/media locally), while OpenPhonex Cloud still manages the API account, carrier/provider numbers, billing, the compliance workflow, and abuse controls. Use this when call media must stay in your own network but you still want OpenPhonex to own numbers, compliance, and provider operations.

Every number and gateway response includes provider_account_id and provider_account, so you can always tell managed, customer-BYO, self-managed provider, and sandbox accounts apart.

Credentials for a self-hosted gateway

An inbound self-hosted gateway holds one runtime credential: the deployment-bound tgw_ token. Create it in the Gateway Tokens card after the gateway deployment is registered. Its one-time reveal includes this exact line; replace the blank OPENPHONEX_GATEWAY_API_TOKEN= assignment in the gateway host's .env.livekit with the whole revealed line:

OPENPHONEX_GATEWAY_API_TOKEN=tgw_...

That token performs the whole call path: the number-rules sync, the inbound route, the resulting call's runtime-config read, and MCP tools/list schema discovery. Each of those derives its organization server-side from the token row — nothing the host sends can name a workspace — so a compromised gateway reaches the workspace its own deployment belongs to and no further.

Two limits make that boundary real rather than nominal:

  • tools/list is the only MCP method a gateway token may call. It returns the static tool catalogue and reads no workspace row; tools/call and the resource methods stay refused, so a stolen tgw_ cannot send an SMS or read a recording through MCP.
  • The token's kind_allowlist carries call.inbound_route. Mint one without that entry and the deployment cannot route calls at all — which is how you quarantine a suspect host without touching the rest of the workspace.

Set OPENPHONEX_ORGANIZATION_ID to the workspace that registered the gateway and owns the managed DID; a mismatch with the token's own organization is refused before the call is routed. The smoke test checks the token on both surfaces a call needs it for (number-rules and tools/list). Neither is a DID check: tools/list has no target number, so the first inbound route is what proves the routing authority end to end.

The plaintext value is revealed only once and never appears in gateway-token list views. Revoke and replace an exposed value instead of expecting it to be shown again.

One-time bootstrap: two values

The open-source gateway ships selfhost-bootstrap.sh beside its compose file. Run it before docker compose up with exactly two inputs — the control-plane URL and one organization API key carrying gateways:write:

OPENPHONEX_CONTROL_PLANE_URL=https://api.openphonex.com \
OPENPHONEX_BOOTSTRAP_API_KEY=tai_... \
  bash selfhost-bootstrap.sh

It registers the deployment, mints the tgw_ token (with no expiry — an expiry without auto-rotation only manufactures a lockout), resolves the organization, picks the agent-routed number when there is exactly one, and generates the LiveKit key pair into livekit.yaml, sip.yaml and .env.livekit identically. The bootstrap key is read from the environment (or the terminal, without echo) and is never written to the host, so retiring it is one revoke in the dashboard and nothing else.

It deliberately does not derive your carrier's SIP signalling ranges: the control plane holds none, and a guessed allowlist produces a stack that reports healthy and never rings.

What a new gateway token carries

A token minted without an explicit scopes list carries only the two originator scopes, gateways:report and calls:authorize. Both selfhost-bootstrap.sh and the workspace's Gateway Tokens action request the full runtime set instead — adding number-rules:read, worker_jobs:enqueue, artifacts:upload and component_credentials:lease — so a gateway either of them sets up is complete. A client that mints its own token must ask for every scope its host needs. Tokens that already exist keep the scopes they were stored with.

Your gateway publishes no URL

A self-hosted gateway does not need a public hostname, a tunnel, or a TLS certificate for OpenPhonex's benefit. Your carrier reaches it over SIP, and the control plane never dials into it — every connection runs the other way, from your gateway out to the API. There is nothing to port-forward and no inbound address to register.

Registration reflects this: a deployment carries no public gateway URL and no local status URL. Both fields were removed from the workspace form and from the deployment resource. A public_url or local_status_url sent on POST /v1/gateway-deployments or on a health report is still accepted, so an older client keeps working, but it is ignored and never returned.

Outbound: which originator dials

A deployment's outbound_execution_mode decides how its outbound calls are placed. Read it from the deployment (GET /v1/gateway-deployments/{gateway_id}) and change it with PATCH /v1/gateway-deployments/{gateway_id}/outbound-execution-mode (gateways:write). Only a self-hosted deployment may be local.

localqueue (the default)
Who asks for the callthe gateway host itselfany client with an API key, through POST /v1/calls
Who dials itthe same gateway hostthat host's --profile outbound worker, after claiming the job
Credentials on the hostthe tgw_ token onlythe tgw_ token and an organization-scoped tai_ key

Both modes describe a gateway you run yourself. A workspace with no self-hosted deployment has nothing to choose here: its managed outbound calls are dialed by OpenPhonex's shared hosted dialer, which you neither run nor configure.

local exists because on a self-hosted box the requester and the dialer are the same machine, so the shared job queue buys nothing and costs an organization-scoped credential. The host calls POST /v1/gateway-deployments/{gateway_id}/outbound-calls with its own tgw_ token; OpenPhonex runs the same admission a remote POST /v1/calls gets — the outbound authorization decision, the kill switches and caps, the wallet reserve, the call record — and answers with the admitted call plus a single-use authorization bound to that one (gateway, organization, from-number, to-number, call). Nothing on the host can reach media without it, and a blocked decision returns no authorization at all.

Remote origination fails closed in local mode

Once a workspace has a local-mode deployment and no queue-mode deployment left, POST /v1/calls for one of its numbers is refused with a message naming the local flow, rather than queueing a job nothing would claim. The refusal happens before the wallet reserve, so no money is held. Job claims are per-organization, so one remaining queue-mode deployment keeps every project working.

The organization-scoped worker key

OPENPHONEX_WORKER_API_TOKEN is an organization-scoped tai_ key. Inbound calling no longer needs it — the change above is additive, so an existing deployment that still sets one keeps working unchanged — and the only part of the shipped stack that still reads it is the legacy queue outbound mode's job-claiming worker, because claiming a call job somebody else requested is worker_jobs:* authority over the whole workspace queue that a deployment-bound token deliberately does not carry.

An inbound-only host, and a host in local outbound mode, therefore hold no organization-wide credential at all.

Later: fully self-hosted / BYO provider

A fully self-hosted / bring-your-own-provider path (your own carrier or a future provider account) is designed into the same open-source stack, but it is a later path — not one of the two MVP launch modes.

Picking a mode

fully_managed_cloudmanaged_numbers_self_hosted_gateway
API accountOpenPhonexOpenPhonex
Numbers & provider opsOpenPhonexOpenPhonex
Billing & complianceOpenPhonexOpenPhonex
Media gatewayOpenPhonex (hosted)You (Docker/media local)
Fastest to liveYes
Media stays in your networkYes

To scaffold the self-hosted path, the API returns an install plan: GET /v1/self-hosted/install-plan?deployment_model_id=managed_numbers_self_hosted_gateway lists the customer/operator steps, required env names, the compose command, and the doctor command.

On this page