Skip to content
DomainKit
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Last updated on

Was this page helpful?