Connections
The connection lifecycle, the authoritative record set, tenancy, and drift.
A connection is one customer domain being onboarded under an application. Its
id doubles as the jobId the widget and webhooks report. Every connection
carries an authoritative desired record set (the records that point the
domain at the edge) and a status.
The connection lifecycle
The status machine is deliberately small:
apply a rail (OAuth / one-click setup / API token)
pending ─────────────────────────────────────────────▶ propagating ──▶ live
│ │
└──────────── manual: records seen in public DNS ─────────┘ │
│
propagating > 24h with records unseen ──▶ failed
│
:recheck / re-create / edge traffic ──▶ pending ◀───┘| Status | Meaning |
|---|---|
pending | Created; records not yet seen. Fast backoff for 72 hours, then it stays pending with error_code: setup_incomplete and is re-checked every six hours. Never fails on its own. |
propagating | Records were written (by a rail) and are being checked against public DNS. |
live | Every desired record resolves to its intended value. The edge serves TLS for the host (only when the record set includes an edge record — an email-only set is verified but never served). |
failed | A propagating connection's records never resolved within 24 hours (propagation_timeout), or a rail rejected the write. Not terminal — see Retrying. |
There is no separate ownership-challenge step. Control is proven by the rail
itself: an OAuth authorization, a one-click setup apply, or a scoped API token
each demonstrate the customer controls the zone. In the manual flow, the poller
simply waits for the records to appear — a pending connection has no
deadline (after 72 hours it is merely re-checked less often, with an honest
setup_incomplete diagnosis).
Retrying
failed is a diagnosis, not a tombstone. Three things revive a failed
connection into pending with a fresh window and a cleared diagnosis:
POST /v1/connections/{id}:recheck (the console's Resume / Check now),
re-POST /v1/connections for the same domain (returns the revived row with
200), and real traffic arriving at the edge for the host. The diagnosis that
led to the failure stays readable in the meantime as error_code /
error_message, and rides the webhook events.
The authoritative record set
GET /v1/connections/{id}/records returns the single source of truth the widget
renders, the OAuth callback writes, and the poller verifies. Before anything is
applied, the control-plane synthesizes the default edge record
(CNAME <host> → edge.customdomain.ai, TTL 3600, or your configured edge
target). Once a rail writes records, the stored set is returned instead.
Propagation → live
A background poller (running automatically on a 1-minute interval) drains
propagating and pending connections, resolving each desired record against
public DNS with value checking (a CNAME must resolve to the expected target;
an A/AAAA must contain the exact address). When all records resolve, the
connection flips to live and connection.live fires — exactly once, guarded by
a compare-and-swap so the API and the worker can't double-fire.
Idempotency & tenancy
POST /v1/connections is idempotent per application + domain: if the app already
has a non-failed connection for the domain, that connection is replayed (200)
instead of a duplicate being created. Every object is tenant-scoped — an sk_
API key sees all its tenant's connections; a widget JWT sees only its
application's. Cross-tenant ids return 404, never another tenant's data.
Drift
After go-live, the Monitor sweep
re-checks declared records against live DNS. If a record disappears or is
repointed, domain.record_missing fires; when it returns, domain.record_restored
fires — so you can alert the customer. The synchronous POST /v1/monitor:check
(and the SDK's checkRecords) runs the same comparison on demand.