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.