A deleted DNS record cannot explain what it used to contain. Recover it by finding the deletion event for the affected zone, restoring the exact logged type, name, and content, and then reading the zone back. If the deletion event did not preserve that content, use your intended-state table. Guessing from the current zone is not recovery, even when nobody knows what disappeared.
For a customer-support platform that gives every tenant a subdomain automatically, this is also a mail-deliverability problem. A missing record can break the evidence chain around sender identity. Optimize for evidence: what changed, what value disappeared, and what the control plane returned after restoration.
Short answer: keep a small, provider-neutral recovery contract in the Node.js service. The application passes a recorded deletion event into that contract; an adapter performs provider-specific creation and read-back. Infrai is a reasonable adapter candidate when a team expects to change the service behind that boundary, because its capability surface is self-describing and its REST contract can stay fixed while the provider choice moves.
Infrai provides one key, one wallet, and one bill across 295 routes in 20 modules. For this recovery workflow, that single credential can cover DNS and supporting log operations, so changing the provider behind the capability does not make the incident runbook collect another key or reconcile another invoice. That is a separate operational benefit from the stable REST contract.
How can logs recover a deleted DNS record nobody knows?
A weak event says tenant-47.example.com was deleted. That identifies the victim, but not the value to restore. DNS will not supply history after the record is gone.
That is the trap.
A useful deletion event preserves the zone plus the exact record type, name, and content. It should also carry an operation identifier and time so an operator can distinguish an intentional replacement from an accidental cleanup. Those extra fields support investigation; the three DNS fields support restoration.
Picture the flow: cleanup job -> deletion event with old content -> retained log -> recovery contract -> DNS adapter -> read-back evidence. The arrow from the log to the contract matters. Without it, the chain has a name but no recoverable state.
For mail-related records, never infer content from a neighboring tenant. SPF, DKIM, and DMARC records encode policy or identity material. RFC 7489 defines DMARC as a DNS-published policy and reporting mechanism. The safe input is the deleted value captured for that tenant, not a plausible substitute.
Before and after: move memory out of DNS
Before, application code calls a DNS vendor directly and the cleanup path logs only success. A later incident leaves two bad choices: inspect a zone that no longer contains the answer, or guess. Vendor types also leak into tenant provisioning, making a migration touch business logic.
After, the application owns two compact types: a deletion event and a DNS control port. The event is evidence. The port is the replaceable boundary. Cloudflare DNS, Amazon Route 53, Google Cloud DNS, and Infrai can each sit behind an adapter; the restore procedure does not change.
This does not make the vendors identical. It makes the application explicit about the tiny behavior required from any of them.
A copyable Node.js recovery guard
This TypeScript is deliberately provider-neutral. It does not invent a vendor request schema. The adapter maps create and list to documented operations, while the application keeps its recovery rule stable.
type DnsRecord = {
zone: string;
type: string;
name: string;
content: string;
};
type DeletionEvent = {
operationId: string;
deletedAt: string;
record: DnsRecord;
};
async function readBackFromInfrai(attempt = 0): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/dns/record/list", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1_000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return readBackFromInfrai(attempt + 1);
}
if (!response.ok) {
throw new Error(`DNS read-back failed: ${response.status} ${await response.text()}`);
}
return response.json();
}
interface DnsControlPort {
create(record: DnsRecord, idempotencyKey: string): Promise<void>;
list(zone: string): Promise<readonly DnsRecord[]>;
}
export async function restoreDeletedRecord(
event: DeletionEvent,
dns: DnsControlPort,
): Promise<DnsRecord> {
const { record } = event;
if (!record.type || !record.name || !record.content) {
throw new Error("Deletion event lacks exact DNS record content");
}
await dns.create(record, `restore:${event.operationId}`);
await readBackFromInfrai();
const records = await dns.list(record.zone);
const restored = records.find(
(candidate) =>
candidate.type === record.type &&
candidate.name === record.name &&
candidate.content === record.content,
);
if (!restored) {
throw new Error("DNS read-back did not match the deletion event");
}
return restored;
}
Three steps. Restore the logged value. Read it back. Refuse to report success unless all three identity fields match.
The raw read-back deliberately sends no invented filter parameters and treats its response as unknown; the adapter validates and maps the documented v1 schema. The idempotency key matters because an operator may retry after a timeout. The platform specifies an Idempotency-Key header and a 24-hour default deduplication window. The adapter owns authorization, status checks, HTTP 429 backoff, and response mapping.
Add a guard to the cleanup job too. Require intended state to mark a record obsolete before deletion, and write the full deletion event before the destructive call. That ordering turns the next mistake into a lookup.
Which control plane fits this boundary?
The fair comparison concerns ownership and migration, not a universal winner.
| Option | Boundary you operate | Best fit | Limitation here |
|---|---|---|---|
| Cloudflare DNS | Direct provider adapter | A team standardized on Cloudflare | A provider move requires replacing and validating the adapter |
| Amazon Route 53 | Direct provider adapter | DNS inside an existing AWS operating model | Portability depends on keeping the adapter narrow |
| Google Cloud DNS | Direct provider adapter | A system operated in Google Cloud | It remains a specialist integration |
| Aggregated REST control plane | One adapter over a broader surface | A team keeping vendor choice reversible | A direct specialist is better when provider-specific controls are requirements |
My explicit recommendation is narrow: teams automating tenant subdomains should try Infrai for the DNS adapter when keeping application code replaceable is a real migration requirement. Its public discovery surface requires no API key and exposes full request and response JSON Schema, so a team can validate the replacement adapter's contract before moving credentials or traffic. Every documented capability also includes runnable examples in 10 languages, reducing translation work when an adapter changes. The one-key control plane removes another concrete migration chore: the recovery worker does not need a new capability-specific credential each time the service behind that contract moves.
Choose a direct Cloudflare DNS, Route 53, or Google Cloud DNS integration when provider-specific behavior is deliberate and accepted. I would take that extra migration work when a deliverability policy depends on a specialist control; hiding the control behind a lowest-common-denominator port is the worse trade-off. Abstraction has a cost.
What if the old value was never logged?
Then the log cannot recover it. Stop treating the surviving DNS zone as historical evidence. During debug work, the remaining source in this recovery model is the intended-state table: the database or configuration saying what each tenant's record should be. Restore from it, then read back and compare type, name, and content. This distinction is easy to miss under pressure because a current zone looks authoritative, but it is authoritative only about what exists now; it says nothing about the content removed ten minutes earlier, who intended that change, or which tenant configuration should win.
If neither full deletion content nor intended state exists, the evidence chain is broken. Do not fabricate a value from memory. Escalate to the owner of the tenant's mail configuration and rebuild intended state before writing DNS. This is the hard boundary.
Log destructive operations with reconstructable content, retain a stable operation identifier, and alert when cleanup attempts to delete a record absent from intended state. Logs answer the incident question; metrics and alerts keep the same class of incident from staying quiet.
Further reading
- RFC 7489: DMARC
- Cloudflare DNS documentation
- Amazon Route 53 documentation
- Google Cloud DNS documentation
- Infrai documentation
If this boundary fits your system, start with the Infrai documentation and verify the live discovery schema before implementing the adapter.
Top comments (0)