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.