Custom Domain
Getting started

Quickstart

Connect your first custom domain end to end in about five minutes.

Connect your first custom domain end to end. This uses the API directly; to embed the UI instead, see The widget SDK.

0. Run the stack (local)

make up   # docker compose: Postgres + control-plane + edge

The control-plane listens on http://localhost:8080. For real deployments see Self-hosting.

1. Provision a tenant, app, and key

One call creates a tenant, its owner, a default application, and a long-lived API key. It's guarded by the provision secret (X-Provision-Secret; in local dev the management/provision secrets may be unset).

curl -X POST http://localhost:8080/v1/tenants:provision \
  -H "X-Provision-Secret: <PROVISION_SECRET>" \
  -H "Content-Type: application/json" \
  -d '{"tenant_name":"Acme","app_name":"Acme Sites","owner_email":"[email protected]"}'
# → { "tenant_id": "...", "application_id": "...", "api_key": "sk_...", "client_secret": "..." }

api_key and client_secret are shown once — store them. (You can also create tenants and applications individually via POST /v1/tenants and POST /v1/applications with the management key.)

2. Mint a widget token

Trade the app's client_secret for a short-lived widget JWT (server-side only):

curl -X POST http://localhost:8080/v1/tokens \
  -H "Content-Type: application/json" \
  -d '{"application_id":"<APP_ID>","client_secret":"<CLIENT_SECRET>","domain":"app.customer.com"}'
# → { "token": "eyJ...", "token_type": "Bearer", "expires_in": 3600 }

3. Detect the domain

curl -X POST http://localhost:8080/v1/domains:check \
  -H "Authorization: Bearer <WIDGET_JWT>" \
  -H "Content-Type: application/json" \
  -d '{"domain":"app.customer.com"}'

The response tells you the setup_type, whether oauth_available / domain_connect rails are on offer, capability flags, and any record_conflicts.

4. Create a connection

curl -X POST http://localhost:8080/v1/connections \
  -H "Authorization: Bearer <WIDGET_JWT>" \
  -H "Content-Type: application/json" \
  -d '{"domain":"app.customer.com"}'

The response carries the connection id (also the jobId), its status (pending), and the authoritative desired records — by default a CNAME app.customer.com → edge.customdomain.ai.

5. Apply the records

Pick a rail. The manual path needs nothing more than the customer adding the records above. To write them programmatically with a scoped provider token (Rail B):

curl -X POST http://localhost:8080/v1/connections/<ID>/apply \
  -H "Authorization: Bearer <WIDGET_JWT>" \
  -H "Content-Type: application/json" \
  -d '{"provider":"cloudflare","zone":"customer.com","credential":{"token":"<scoped>"},
       "records":[{"type":"CNAME","host":"app","value":"edge.customdomain.ai"}]}'

The connection moves to propagating. (Rail C is oauth:start, Rail A is domainconnect:start — see Apply records & go live.)

6. Go live

The background poller (running automatically) promotes the connection to live once every desired record resolves. Watch it:

curl http://localhost:8080/v1/connections/<ID> -H "Authorization: Bearer <WIDGET_JWT>"

When status is live, the edge issues the TLS certificate on the next handshake and proxies https://app.customer.com to your origin. Register a webhook for connection.live to be notified.

Next steps

  • Embed the widget so customers do steps 3–6 themselves.
  • Register webhooks to react to go-live and drift.
  • Review the connect rails and what each needs to run in production.

On this page