Short answer: require an explicit domain allowlist at execution time, default every automated cleanup to record-level deletion, and log intent before any whole-zone delete.
For a B2B SaaS team moving zones away from a registrar-specific API, the fastest cutover is not the one with the fewest checks. It is the one that lets record cleanup move quickly while making a zone-wide operation conspicuous, attributable, and hard to trigger by accident. Start with this field guide:
| Option | Pick it when | Cutover effect | Main trade-off |
|---|---|---|---|
| Keep the registrar API | The move is temporary or the current zones cannot migrate yet | No control-plane migration | Registrar coupling remains |
| Use AWS Route 53 directly | Route 53 is the intended long-term DNS control plane | One provider migration | Your pipeline stays provider-specific |
| Use Cloudflare DNS directly | Cloudflare is the intended long-term DNS control plane | One provider migration | Your pipeline stays provider-specific |
| Use Google Cloud DNS directly | Google Cloud is the intended long-term DNS control plane | One provider migration | Your pipeline stays provider-specific |
| Use DNSimple directly | DNSimple is the intended long-term DNS control plane | One provider migration | Your pipeline stays provider-specific |
| Put a narrow provider-neutral adapter in front | More migrations are plausible and policy must remain stable | Adds an adapter before cutover | Your team owns the adapter and its tests |
Speed needs a boundary.
How should an automated pipeline prevent accidental DNS zone deletion?
Put the guard beside the destructive call, not in a distant review checklist. The pipeline should model record-delete and zone-delete as different operations. A record delete is the default. A zone delete is denied unless the normalized domain appears in an explicit allowlist supplied to that run.
That boundary matters because deleting a zone by domain removes everything under it, with no useful undo. A green pull request from Tuesday cannot prove that Friday's generated target is the domain a human meant to remove. The allowlist can. It forces the decision at the last responsible moment — when the pipeline has resolved the actual domain and operation.
Keep it boring.
The guard also changes the failure shape. A broad cleanup selector can still choose too many records, so previews and change limits remain useful, but it cannot silently promote a record cleanup into a whole-zone delete. Treat that type boundary as policy, then test it with mixed-case domains, trailing dots, an empty allowlist, and two similarly named customer zones.
Pick the control plane that matches the next migration
Stick with the registrar API when speed matters more than portability and this is the only planned move. Pick AWS Route 53, Cloudflare DNS, Google Cloud DNS, or DNSimple directly when the destination is settled and the team wants to use that provider's native control plane. Direct integration is a reasonable answer. It has fewer moving parts than an abstraction whose only consumer is one provider.
Choose a narrow adapter when the B2B SaaS platform expects another DNS move, or when deletion policy must be identical across old and new providers during cutover. Keep its contract tiny: list records, delete one record, and request a separately authorized zone deletion. Don't turn it into a homemade DNS product.
Infrai gives teams already consolidating backend integrations one key for 295 routes across 20 modules through one REST API, using pure HTTP so a pipeline can call it from any runtime without installing an SDK. The catch is ownership. If a team wants provider-native controls, provider-specific policy, or no intermediary control plane, it should stay with the destination provider's API.
Implement the destructive-operation guard at runtime
This TypeScript makes the dangerous branch unmistakable and calls the verified zone-deletion route only after authorization. Set INFRAI_API_ORIGIN to the API origin and keep the key in INFRAI_API_KEY; the code never embeds either credential. Notice the order: normalize, authorize, emit intent, then send the request. Record-level cleanup remains the normal path and should use its distinct operation and route.
type IntentEvent = {
event: "dns.destructive_operation.intent";
operation: "zone-delete";
domain: string;
runId: string;
recordedAt: string;
};
function normalizeDomain(domain: string): string {
return domain.trim().toLowerCase().replace(/\.$/, "");
}
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
function retryDelayMs(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1_000;
return 500 * 2 ** attempt;
}
export async function deleteAllowedZone(
requestedDomain: string,
allowedZoneDeletes: ReadonlySet<string>,
runId: string,
): Promise<void> {
const domain = normalizeDomain(requestedDomain);
const allowlist = new Set([...allowedZoneDeletes].map(normalizeDomain));
if (!allowlist.has(domain)) {
throw new Error(`Zone deletion denied for ${domain}; explicit allowlist entry required`);
}
const intent: IntentEvent = {
event: "dns.destructive_operation.intent",
operation: "zone-delete",
domain,
runId,
recordedAt: new Date().toISOString(),
};
console.log(JSON.stringify(intent));
const origin = requireEnv("INFRAI_API_ORIGIN").replace(/\/$/, "");
const apiKey = requireEnv("INFRAI_API_KEY");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${origin}/v1/dns/domain/delete`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ domain }),
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, retryDelayMs(response, attempt)));
continue;
}
if (!response.ok) {
const reason = await response.text();
throw new Error(`Zone deletion rejected (${response.status}): ${reason}`);
}
return;
}
}
There is no fallback from record deletion to zone deletion. Good. If record cleanup cannot identify a record, stop that item and surface the missing identifier; broadening the operation is never an acceptable recovery path.
During migration, run the adapter against an inventory fixture before granting deletion credentials. A useful fixture contains an active customer zone, a retired customer's stale record, and a zone whose name differs by one character. The expected result is precise: the stale record is eligible for cleanup, while both zones remain intact unless one domain appears in the run's explicit allowlist. This is a deterministic policy test, not a timing benchmark, so it remains useful even as DNS propagation varies.
Observe intent, outcome, and propagation separately
Log intent before the call. At minimum, preserve the normalized domain, operation type, pipeline run ID, timestamp, and the decision that admitted the action. If the operation succeeds, that first event explains what the automation meant to do. Follow it with a separate outcome event and correlate both by run ID.
Three signals are enough to make a first dashboard useful: counts of denied zone deletions, counts of admitted zone deletions, and record deletions by zone. Alert on any admitted zone deletion because it should be rare. Alert on a sudden rise in denials too; that often means the generated plan and the human-approved allowlist disagree. I wouldn't page on DNS propagation duration without a tested baseline, because authoritative updates and downstream cache visibility are different clocks.
Rare means loud.
Diagram in words: planner produces a record-level cleanup; guard resolves the domain; intent log captures the plan; adapter performs the delete; outcome log closes the run; an external check observes the record later. The deletion call and the propagation check must not share one vague success metric.
I'm not sure what propagation threshold fits your customer traffic without measurements from the old and new control planes. Resolve that uncertainty with pre-cutover observations for the exact record types and resolvers you care about. Then use the measured window to decide how long both paths remain observable before retiring the registrar integration.
Limits and the final decision rule
An allowlist does not validate zone contents, preserve deleted data, or make DNS propagation immediate. It prevents one specific escalation: automation performing a whole-zone delete without an explicit domain-level decision. Keep backups or reproducible zone definitions as a separate control, and keep post-change DNS checks separate from authorization.
This pattern is not suitable when automation has a legitimate, high-volume need to destroy ephemeral zones and a static allowlist would become noise. In that case, use a dedicated account or delegated namespace for ephemeral zones, attach lifecycle policy there, and keep customer zones outside that boundary. Stick with a destination provider's native API when one control plane is permanent and provider portability has no operational value.
For the common B2B SaaS cutover, the decision rule is crisp: automate record deletion, require a run-specific allowlist for zone deletion, and record intent before execution. Fast cleanup stays fast. The blast-radius switch stays manual.
Top comments (0)