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

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 effect
pnpm add domainkit effect
yarn add domainkit effect
bun add domainkit effect
nub add domainkit effect
aube add domainkit effect

effect 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 false

The 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

The entry points reference lists what each import carries, and testing covers the fakes this tutorial used.

Last updated on

Was this page helpful?