Skip to content
DomainKit
Esc
navigateopen⌘Jpreview
On this page

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.parse validated a complete requirement, including ownership, purpose, conflict policy, and TTL.
  • Provisioning.create read the provider and produced an immutable plan containing one create.
  • Provisioning.authorize approved the exact plan digest and its create operation.
  • Provisioning.apply revalidated provider state, created the record, and returned a durable receipt.
  • The second Provisioning.create observed the exact record and produced a noop instead 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

Was this page helpful?