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

The transport

Build the browser-side view of your routes, declare which capability groups exist, and adapt a Promise client.

domainkit/client is the browser’s view of the routes you mounted. It holds no credential and knows no provider: it calls your server, and your server calls the provider.

@domainkit/react -> Transport.fromFetch -> your routes -> domainkit/server -> provider

Build one

/** Point it at the path the route group is mounted on. No credential ever reaches this code. */
export const transport = Transport.fromFetch("/api/domainkit");

The base URL is the prefix the route group is mounted at. A relative path is fine when the app and the API share an origin.

/** Headers can be static or resolved per request, for a token your app refreshes on its own. */
export const authenticated = Transport.fromFetch("https://api.acme.dev/domainkit", {
  headers: async () => ({ authorization: `Bearer ${await currentAccessToken()}` }),
});

headers is a HeadersInit or a function returning one, awaited per request, so a token your app refreshes is read at call time rather than captured once.

Cookies are the usual answer instead. The provider callback is a top-level navigation, so a host whose Identity reads a cookie needs no headers here at all.

Capability groups

/**
 * A host that mounts only part of the group declares only those groups. `Transport.capabilities`
 * reports what is there, and `@domainkit/react` renders only those parts of the flow.
 */
export const connectOnly = Transport.fromFetch("/api/domainkit", {
  capabilities: ["connection"],
});

export const declared = Transport.capabilities(connectOnly); // ["connection"]
Group Methods
connection inspect, discover, zones, start, reconnect, attach, detach, disconnect
provisioning plan, approve, reject, apply, attempt, receipt
verification observe, latest
cleanup plan, approve, reject, apply

Every group is optional. A host that mounts only the connection routes gets a transport that typechecks and a UI that renders only the connection step, because @domainkit/react reads Transport.capabilities(transport) and skips the rest.

Calling it

/** Every method is an `Effect` that fails with the `DomainKit.Error` the lifecycle raised. */
export const plan = Effect.gen(function* () {
  const provisioning = transport.provisioning;
  if (provisioning === undefined) return null;
  return yield* provisioning.plan({
    domain: "app.example.com",
    requirements: [DnsRecord.cname({ name: "app.example.com", target: "edge.acme.dev" })],
  });
});
/**
 * Verification takes the requirements to look for. Without them the server uses the domain's
 * latest provisioning receipt, and a domain with neither answers `InvalidInput`.
 */
export const readiness = Effect.gen(function* () {
  const verification = transport.verification;
  if (verification === undefined) return null;
  return yield* verification.observe("app.example.com", {
    requirements: [DnsRecord.cname({ name: "app.example.com", target: "edge.acme.dev" })],
  });
});
/** The same transport in Promises, for a component tree that does not run Effect. */
export const api = Transport.toAsync(transport);

export const inspect = async (domain: string) => api.connection?.inspect(domain);

Every method returns Effect<A, DomainKit.Error> with no requirements, so @domainkit/react runs them without an Effect runtime in the tree. Transport.toAsync gives the same methods as Promises that reject with the same error.

Transport.fromAsync goes the other way, for a host whose client is already Promise-shaped.

Failures

A failure arrives as the DomainKit.Error the lifecycle raised, reason intact, so a Conflict still carries its conflicting operations and the UI can list them.

A reply the transport cannot read as one came from in front of your server. It is classified from the status instead, and every result names the transport’s base URL, so a proxy answering HTML is obvious in the message.

Status Becomes
401 Unauthenticated
403 Forbidden
409 ProviderConflict
429 and 5xx ProviderUnavailable, retryable, with retryAfterMs from retry-after
anything else ProviderRejected, carrying the status as its code

In tests

/**
 * The whole lifecycle behind a `Transport.Interface`, over an in-memory server, recording every
 * call. Declare fewer capabilities to render a UI against a host that mounted only part of the
 * group.
 */
export const transport = Testing.transport({ capabilities: ["connection", "provisioning"] });

export const callsSoFar = () => transport.calls.map((call) => call.method);

Testing.transport runs domainkit/server over memory storage and a fake provider in the same process, so a component test drives the real lifecycle instead of a stub.

The domainkit/client reference lists every method on every group.

Last updated on

Was this page helpful?