Connections and attachments
How a provider credential becomes a connection, how a domain attaches to a zone, and how a second domain reuses what the customer already connected.
This page explains how a customer’s provider access is stored and reused: an authorization, a connection, and an attachment, each answering a different question.
authorization the credential, sealed, plus the provider account context
|
connection the principal-facing handle over one authorization
|
attachment one domain bound to one connection and one exact zone
One authorization can back several connections, and one connection can serve several domains. DomainKit never deduplicates by provider account: a second organization connecting the same provider account gets its own authorization from its own fresh proof.
Inspecting a domain
/** Everything a settings screen needs: the attachment, the connection, and what else is on offer. */
export const state = Effect.map(Connect.inspect(domain), (snapshot) => ({
attached: snapshot.attachment !== null,
provider: snapshot.authorization?.provider ?? null,
revocation: snapshot.authorization?.revocation ?? null,
lastReceiptId: snapshot.lastReceiptId,
reusable: snapshot.reusable.map(({ connection, provider }) => ({
connectionId: connection.id,
provider,
})),
offered: snapshot.providers.map(({ id, methods }) => ({ id, methods: methods.length })),
}));The snapshot also carries the provider catalog: each provider’s id, name, and its auth methods with their labels, docs URLs, and token fields. A connect form renders from that instead of hard-coding provider names.
Connecting
A token method finishes in one call. OAuth and marketplace installs answer with a redirect and
finish at the callback. The domain is optional: the account is what the customer is granting, and a
start without a domain connects it and attaches nothing.
/** A token method connects in one call: `Started.Connected` carries the connection right away. */
export const withToken = Connect.start({
provider: "cloudflare",
method: Connect.Method.token({ token: "cf_api_token", accountId: "acct_1" }),
domain,
});/**
* OAuth and marketplace installs answer `Redirect`. Send the customer to `authorizationUrl`; the
* provider drives the browser back to `/callback/:provider`, which calls `Connect.complete`.
*/
export const withOAuth = Effect.map(
Connect.start({
provider: "cloudflare",
method: Connect.Method.oauth({ returnTo: "/settings/domains" }),
domain,
}),
(started) => (started._tag === "Redirect" ? started.authorizationUrl : null),
);
export const finish = (continuationId: string, callbackUrl: string) =>
Connect.complete({ continuationId, callbackUrl });Connect.start answers one of three things:
Started |
Means |
|---|---|
Connected |
The connection exists, and the domain is attached when one was given |
Redirect |
Send the customer to authorizationUrl; the callback finishes it |
SelectionRequired |
The credential reaches several matching zones; pick one and attach |
The continuation created by start is spent exactly once, so a replayed callback fails NotFound
rather than connecting twice.
Listing the zones an owner reaches
Connect.zones({ provider? }) lists every zone the principal’s connections can serve, ordered by
zone, each with the connection that reaches it and the label the provider gave it. A connection
whose credential the provider turned down is reported once in the sibling connections array with
status reconnect, and contributes no zones, so one dead account never costs the customer the
others. Each entry in that array carries the label the provider gave the account at connect
time, so a list names accounts without asking the provider again; a provider that names no single
account leaves it null. The reply also carries the provider catalog, so a picker can offer a new
account without a second call.
Reusing a connection
The second domain on a provider the customer already connected should not ask them to connect
again. Connect.discover resolves the domain’s authoritative nameservers, lists the zones each of
the principal’s connections can reach, and answers Resolved, SelectionRequired, or NotFound.
/**
* Before offering the provider list, ask which connection this owner already has for the domain.
* `Resolved` means the second domain needs no connect step at all.
*/
export const reuseExisting = Effect.gen(function* () {
const discovery = yield* Connect.discover(domain);
switch (discovery._tag) {
case "Resolved":
return yield* Connect.attach({
connectionId: discovery.connectionId,
domain,
target: discovery.target,
});
case "SelectionRequired":
return discovery.candidates.map(({ connectionId, target }) => ({
connectionId,
zone: target.zone,
label: target.label,
}));
case "NotFound":
// `host` names the registered provider whose nameservers serve the domain, or is `null`.
return { host: discovery.host?.provider ?? null, nameservers: discovery.nameservers };
}
});The closest zone wins. When several connections hold that zone, a connection whose reported
nameservers cover every authoritative nameserver wins; otherwise the answer is
SelectionRequired and the caller picks.
When no connection reaches the domain, the NotFound answer carries the authoritative nameservers
and host. host.provider is the one registered provider whose declared nameserver suffixes cover
every one of them, matched on trailing labels and ignoring case. Cloudflare declares
ns.cloudflare.com and Vercel declares vercel-dns.com, so a connect screen can name the provider
that already serves the domain before the customer picks one. host is null when no provider
matches or more than one does.
Attaching
/**
* Attaching binds one domain to one zone. When a credential reaches several matching zones the
* caller picks, and `attach` is called again with the chosen target.
*/
export const attach = (connectionId: string) =>
Effect.map(Connect.attach({ connectionId, domain }), (attached) =>
"_tag" in attached ? { choose: attached.candidates } : { attachmentId: attached.id },
);An attachment is exact: one domain, one connection, one zone, and the provider identity that
addresses it. It also keeps label, the name the provider gave the zone at attach time, so a
surface can say where the records go without asking the provider again.
Every session handed to planning, applying, or observing is re-checked against the provider, so a
zone the credential can no longer reach fails NotFound instead of writing somewhere else.
Releasing
/** Detach forgets the domain and leaves the records in DNS; plan a cleanup first to remove them. */
export const release = (attachmentId: string) => Connect.detach(attachmentId);
/** Disconnect detaches every domain and revokes the credential where the method supports it. */
export const revoke = (connectionId: string) => Connect.disconnect(connectionId);Detaching forgets the domain and leaves the records in DNS. Plan a cleanup first when the records should go.
Disconnecting detaches every domain and revokes the credential at the provider where the method supports it. Revocation is two-phase: the row is marked pending, the provider is called outside any transaction, and only then is the row removed. A failure in between leaves a row that recovery finishes on the next connect or disconnect.
Keeping the credential alive
Connect.refresh runs automatically before provider use, ten minutes before expiry by default, and
single-flights through the storage lock so two requests never refresh at once. It answers current,
refreshed, or reconnect.
An access token expiring is not the authorization expiring: a credential with a refresh token stays
usable. A credential the provider will no longer refresh surfaces as the Reconnect reason, and the
host asks the customer to prove the account again.
Connect.reconnect({ connectionId, method }) is what answers that. It puts a fresh credential
behind the authorization the connection already points at, so the connection keeps its id and every
domain on it stays attached. Starting again would create a second account and fail on the first
domain, which is already attached to the connection the provider turned down. The zone check on
every session still applies, so a credential for a different account fails NotFound on a zone it
cannot reach rather than writing somewhere else.
Connect.Policy sets refreshBeforeMs, the continuation lifetime, and allowReuse, which decides
whether one of an owner’s connections may serve a given domain.
Setting up DNS through a provider’s own template protocol instead is Domain Connect, which DomainKit does not implement.