Skip to content
DomainKit
Esc
navigateopen⌘Jpreview
On this page

Cleanup

Why removing records is its own plan, what a receipt proves, and what happens to a record someone edited by hand.

Cleanup removes only what a receipt proves DomainKit created, and it runs through the same three verbs as provisioning: plan, approve, apply. The difference is where the plan comes from.

/**
 * Cleanup is planned from an apply receipt, never from requirements. Each applied record is read
 * back by its provider record id: still an exact match becomes `Delete`, anything else `Conflict`.
 */
export const planFromReceipt = (receiptId: Receipt.ReceiptId) => Cleanup.plan({ receiptId });

/** Or from the domain, which uses its latest provisioning receipt. */
export const planLatest = Cleanup.plan({ domain: "app.example.com" });

Each Applied outcome in the receipt names a provider record id. Cleanup reads that record back and turns it into a Delete when it still matches the record the plan wrote exactly. Anything else becomes a Conflict.

/** A record someone edited by hand is a conflict, so cleanup leaves it alone. */
export const review = (plan: Plan.Model) => ({
  deletes: Plan.writes(plan).length,
  leaveAlone: Plan.conflicts(plan).map((conflict) => ({
    record: conflict.record.name,
    reason: conflict.reason,
  })),
});
Conflict reason Means
missing The record is already gone
value-mismatch It no longer matches what the receipt says DomainKit wrote

A record the provider now reports as something DomainKit cannot model is a value-mismatch too: whatever is there, it is not what was written.

A conflict is never deleted. A record the customer edited belongs to the customer.

Its own approval and receipt

/** Cleanup has its own approval and its own receipt under the same attempt rules. */
export const remove = (receiptId: Receipt.ReceiptId) =>
  Effect.gen(function* () {
    const plan = yield* Cleanup.plan({ receiptId });
    const approval = yield* Cleanup.approve(plan);
    return yield* Cleanup.apply(approval);
  });

The cleanup approval binds the cleanup plan’s digest, and the cleanup receipt records what was removed. Declining works the same way and is equally terminal.

export const keepRecords = (plan: Plan.Model) =>
  Cleanup.reject(plan, { reason: "Customer still points traffic at these records" });

Cleanup and detach

Detaching a domain forgets it in DomainKit and leaves every record in DNS. That is the right order for a customer moving a domain elsewhere and the wrong one for a customer who wants their zone tidy.

Plan and apply cleanup first, then detach.

Why not automatic rollback

A failed apply does not undo its earlier writes. A rollback can fail halfway itself, and by the time it runs another actor may depend on the record. A partial receipt says exactly what landed, and cleanup is a separate decision the customer makes with the same review they gave the plan.

Was this page helpful?