---
title: Your first safe DNS plan
description: Create, review, authorize, and apply a DNS plan against an in-memory provider, then confirm that a second plan is an exact no-op.
sidebar:
  label: Quickstart
  order: 2
seo:
  title: Create your first safe DNS plan with DomainKit
---

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

```sh
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`:

```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

```sh
npx tsx quickstart.ts
```

The output should have this shape:

```text
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:

```text
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

<CardGroup cols={2}>
  <Card title="Understand plans and receipts" href="/docs/core/plans" icon="file">
    Learn why authorization is digest-bound and cleanup is a separate lifecycle.
  </Card>
  <Card title="Integrate your application" href="/docs/guides/host-integration" icon="server">
    Map host identity, persistence, endpoints, and credentials around the core lifecycle.
  </Card>
  <ProviderCard title="Connect Cloudflare" href="/docs/providers/cloudflare" provider="cloudflare">
    Replace the in-memory provider with server-side OAuth or a scoped API token.
  </ProviderCard>
  <Card title="Choose a React integration" href="/docs/react/integration-levels" icon="component">
    Use record primitives, complete flows, or host-composed lifecycle models.
  </Card>
</CardGroup>
