Your first plan, approval, and receipt
Run the whole lifecycle against an in-memory provider, then read the plan, the receipt, and the readiness it produced.
You will connect a fake provider, build a plan for two DNS requirements, approve its digest, apply it, and observe the result. No provider account, network request, database, or browser is involved.
Prerequisites
- Node.js 24.10 or newer, or Bun 1.4 or newer.
- A TypeScript project using ESM.
Install
npm install domainkit effectpnpm add domainkit effectyarn add domainkit effectbun add domainkit effectnub add domainkit effectaube add domainkit effecteffect is a peer dependency. The package root is Effect-native, and every step in this tutorial
is an Effect.
Write the program
Create quickstart.ts:
// Plan, approve, apply. Plans are additive and fail closed; conflicts are data, not surprises.
import { Effect, Match } from "effect";
import { Connect, DnsRecord, DomainKit, Plan, Principal, Provision, Verify } from "domainkit";
import { Testing } from "domainkit/testing";
const requirements = [
DnsRecord.cname({ name: "app.example.com", target: "edge.acme.dev", purpose: "Serve your site" }),
DnsRecord.txt({
name: "_acme.app.example.com",
value: "acme-verify=7f3a",
purpose: "Prove ownership",
}),
];
const program = Effect.gen(function* () {
const plan = yield* Provision.plan({ domain: "app.example.com", requirements });
// ^ operations: [Create CNAME, Noop TXT] with a digest the customer approves
const approval = yield* Provision.approve(plan);
const receipt = yield* Provision.apply(approval);
// ^ status: "complete" | "partial", one outcome per operation, safe to retry
const readiness = yield* Verify.observe({ domain: "app.example.com" });
// The same fake provider keeps the CNAME written by apply in this execution.
const secondPlan = yield* Provision.plan({ domain: "app.example.com", requirements });
// ^ operations: [Noop CNAME, Noop TXT], so Plan.isApplicable(secondPlan) is false
return {
firstPlan: {
operations: plan.operations.map(({ _tag, record }) => `${_tag} ${record._tag}`),
isApplicable: Plan.isApplicable(plan),
},
receipt,
ready: readiness.overall === "ready",
nextCheckAt: readiness.nextCheckAt,
secondPlan: {
operations: secondPlan.operations.map(({ _tag, record }) => `${_tag} ${record._tag}`),
isApplicable: Plan.isApplicable(secondPlan),
},
};
}).pipe(
Effect.catchTag("DomainKitError", (error) =>
Match.value(error.reason).pipe(
Match.tag("Conflict", ({ operations }) =>
Effect.fail(`Fix ${operations.length} conflicting record(s) first`),
),
Match.tag("Stale", () => Effect.fail("Provider changed under us; plan again")),
Match.orElse(() => Effect.fail(error.message)),
),
),
);
// A fake provider already holding the TXT record, so the plan shows one Create and one Noop.
const fake = Testing.provider({
zones: ["example.com"],
records: [
{
zone: "example.com",
record: requirements[1] ?? DnsRecord.txt({ name: "x.example.com", value: "x" }),
},
],
});
// The customer connected the provider earlier (a token here; OAuth returns a redirect instead).
const connected = Connect.start({
provider: fake.id,
method: Connect.Method.token("playground-token"),
domain: "app.example.com",
});
export const main = connected.pipe(
Effect.andThen(program),
Effect.provideService(Principal.Service, { ownerId: "org_42", actorId: "user_7" }),
Effect.provide(DomainKit.layerMemory({ providers: [fake], resolver: Testing.resolver() })),
);
if (import.meta.main) {
console.log(JSON.stringify(await Effect.runPromise(main), null, 2));
}Run it
bun quickstart.ts
It prints firstPlan, the receipt, the readiness result, and secondPlan. The two plans list
their operations and whether Plan.isApplicable is true:
{
"firstPlan": { "operations": ["Create CNAME", "Noop TXT"], "isApplicable": true },
"receipt": { "status": "complete" },
"secondPlan": { "operations": ["Noop CNAME", "Noop TXT"], "isApplicable": false }
}
The receipt carries more fields than shown here, and its ids differ on every run.
What you just read
Provision.plan listed the zone and reconciled it against the two requirements. The fake provider
already holds the TXT record, so the plan carries one Create for the CNAME and one Noop for the
TXT, plus a digest over both.
Provision.approve recorded consent for that digest. Provision.apply re-planned the zone, saw the
digest had not moved, wrote the CNAME, and returned a receipt whose outcomes name one Applied
and one Skipped with reason noop.
Verify.observe read the provider through the connection and public DNS through the resolver, and
stored per-requirement readiness with a nextCheckAt for the next look.
The moving parts
| Piece | What it is here |
|---|---|
Testing.provider |
A provider definition over in-memory zones, with a token method |
DomainKit.layerMemory |
Every lifecycle service, memory storage, and a throwaway custody key |
Principal.Service |
Who is acting. Every stored row is scoped by it; there is no default |
Connect.start |
Connects the provider and attaches the domain in one call for a token |
Plan again
The program plans a second time after the apply, in the same run:
// The same fake provider keeps the CNAME written by apply in this execution.
const secondPlan = yield* Provision.plan({ domain: "app.example.com", requirements });
// ^ operations: [Noop CNAME, Noop TXT], so Plan.isApplicable(secondPlan) is falseThe fake provider kept the CNAME the apply wrote, so secondPlan holds Noop CNAME and
Noop TXT, Plan.isApplicable is false, and there is nothing to write. Planning is a read, so it
is safe to run whenever a screen needs to show what would change.
The fake provider lives in memory and is rebuilt on each invocation, so every bun quickstart.ts
run starts from the same zone and prints the same Create and Noop first plan. A real provider
keeps the CNAME between runs.
Next
Plans
Conflicts, partial approval, partial receipts, and stale plans.
Host integration
Mount the routes and swap the fakes for a real provider and database.
The entry points reference lists what each import carries, and testing covers the fakes this tutorial used.