Your first safe DNS plan
Create, review, authorize, and apply a DNS plan against an in-memory provider, then confirm that a second plan is an exact no-op.
In this tutorial, we will create one DNS requirement, review the resulting plan, authorize its exact digest, apply it to an in-memory provider, and plan the same requirement again. The second plan will show a no-op because the desired record already exists.
The exercise does not need a provider account, network request, database, or browser.
Prerequisites
- Node.js 24.10 or newer, or Bun 1.4 or newer.
- A TypeScript project that uses ESM.
Install DomainKit
npm install domainkit effect@rc
effect is a peer dependency. The package root is the canonical Effect API; we will use it for the
whole lifecycle.
Create the exercise
Create quickstart.ts:
import { Effect, Layer } from "effect";
import { Digest, DnsRecord, DomainName, Provisioning } from "domainkit";
import { InMemoryDnsProvider } from "domainkit/testing";
const requirement = DnsRecord.parse({
_tag: "CNAME",
metadata: {
ownership: "customer",
provenance: "product-onboarding",
purpose: "tracking",
},
name: "track.example.com",
policy: "exclusive",
target: "tracking.example.net",
ttl: 300,
});
const runtimeLayer = Layer.merge(InMemoryDnsProvider.layer(), Digest.webCryptoLayer);
const program = Effect.gen(function* () {
const { plan } = yield* Provisioning.create({
requirements: [requirement],
target: Provisioning.Target.ExactZone({
zone: DomainName.parse("example.com"),
}),
});
console.log(
"First plan:",
plan.operations.map(({ _tag }) => _tag),
);
const authorization = yield* Provisioning.authorize(plan);
const receipt = yield* Provisioning.apply({ authorization, plan });
console.log("Receipt:", receipt.status);
const { plan: nextPlan } = yield* Provisioning.create({
requirements: [requirement],
target: Provisioning.Target.ExactZone({
zone: DomainName.parse("example.com"),
}),
});
console.log(
"Second plan:",
nextPlan.operations.map(({ _tag }) => _tag),
);
}).pipe(Effect.provide(runtimeLayer));
await Effect.runPromise(program);
Notice that the provider and Web Crypto capabilities are supplied once around the complete program. Every create, authorize, and apply operation participates in the same in-memory provider lifecycle.
Run it
npx tsx quickstart.ts
The output should have this shape:
First plan: [ 'create' ]
Receipt: complete
Second plan: [ 'noop' ]
The digest and operation identifiers are deterministic but intentionally omitted from this output. The important result is the state transition:
missing record
-> create plan
-> digest-bound authorization
-> apply receipt
-> exact record
-> noop plan
What happened
DnsRecord.parsevalidated a complete requirement, including ownership, purpose, conflict policy, and TTL.Provisioning.createread the provider and produced an immutable plan containing onecreate.Provisioning.authorizeapproved the exact plan digest and its create operation.Provisioning.applyrevalidated provider state, created the record, and returned a durable receipt.- The second
Provisioning.createobserved the exact record and produced anoopinstead of writing again.
The in-memory provider follows the same portable contract as a live provider. It proves the DomainKit lifecycle without introducing credentials, callback routes, persistence, or provider account selection into the first lesson.
Next steps
Understand plans and receipts
Learn why authorization is digest-bound and cleanup is a separate lifecycle.
Integrate your application
Map host identity, persistence, endpoints, and credentials around the core lifecycle.
Connect Cloudflare
Replace the in-memory provider with server-side OAuth or a scoped API token.
Choose a React integration
Use record primitives, complete flows, or host-composed lifecycle models.