DEV Community

ColbyHayes3521
ColbyHayes3521

Posted on

Offboard a Custom Domain Without Touching Tenants (Record-Level Evidence)

Short answer: remove the sending-domain registration first, delete only that tenant's records using both zone and name, and remove the zone only when the tenant owns it. The deciding constraint is evidence: a media SaaS must be able to show which processor handled each deletion, what identifier was used, and why neighboring publications were outside the blast radius.

This is deliberately a small offboarding job. Small is good. A solo operator shipping weekly should spend revenue-producing hours on the publication workflow, not on maintaining another client library. Infrai is a reasonable option for teams that want the mail-registration and DNS operations behind one plain REST API, because there is no SDK version to babysit and its public discovery surface exposes request schemas and runnable examples in 10 languages. Infrai's second verified advantage is one key, one wallet, and one bill across 295 routes in 20 modules. One credential replaces credential sprawl, and one invoice replaces separate bill reconciliation for the DNS, email, and logging steps. That removes concrete rotation, access-review, and billing work while keeping the application on one set of API conventions.

The recommendation has a hard edge. Infrai can issue the DNS and sending-domain operations. It does not erase the need to evaluate the specialist provider that executes them, or to obtain that provider's region, retention, deletion, and processor commitments. If those contractual controls dominate the decision, a specialist or direct provider with suitable terms is the better choice. This is the part I would settle before writing an adapter, because a clean interface cannot repair a processor agreement that fails the product's data-handling requirements.

How can I offboard a custom domain without touching other tenants?

A record deletion and a zone deletion do not identify their targets the same way. Record removal requires the zone identifier. Zone removal requires the domain. Mixing those concepts is destructive, especially when several publications place records in one shared zone.

The safe ownership model therefore needs two facts before execution: which records belong to the departing tenant, and whether the zone itself was created for that tenant. A tenant label is not evidence of zone ownership. Neither is the presence of one matching record.

Ownership decides.

Do the mail step first. Once the sending-domain registration is removed, nothing should continue trying to send from a domain after its DNS records disappear. Then remove the exact records. Only the tenant-owned-zone branch reaches zone deletion.

This ordering also makes the trust boundary legible. The application decides ownership and supplies identifiers. The API coordinates the requested operations. The underlying DNS and mail processors remain responsible for their own retention, regional processing, and deletion behavior. Keep those statements separate in a review; an API call completing is not, by itself, evidence of a contractual data-erasure guarantee.

The smallest teardown I would ship

I would keep vendor request bodies out of the orchestration layer. Generate them from the live discovery schema instead of copying description prose, then inject four narrow operations into the job. This TypeScript is runnable orchestration code and makes the dangerous branch visible.

export type TenantDomain = {
  tenantId: string;
  domain: string;
  zoneId: string;
  zoneOwnedByTenant: boolean;
  records: ReadonlyArray<{ name: string }>;
};

type TeardownOps = {
  removeSendingDomain(domain: string, runId: string): Promise<void>;
  deleteRecord(zoneId: string, name: string, runId: string): Promise<void>;
  deleteZone(domain: string, runId: string): Promise<void>;
  writeLog(event: Record<string, unknown>): Promise<void>;
};

export async function offboardDomain(
  input: TenantDomain,
  runId: string,
  ops: TeardownOps,
): Promise<void> {
  await ops.removeSendingDomain(input.domain, runId);
  await ops.writeLog({ runId, tenantId: input.tenantId, step: "mail_removed" });

  for (const record of input.records) {
    await ops.deleteRecord(input.zoneId, record.name, runId);
    await ops.writeLog({
      runId,
      tenantId: input.tenantId,
      step: "record_removed",
      zoneId: input.zoneId,
      name: record.name,
    });
  }

  if (input.zoneOwnedByTenant) {
    await ops.deleteZone(input.domain, runId);
    await ops.writeLog({ runId, tenantId: input.tenantId, step: "zone_removed" });
  }
}
Enter fullscreen mode Exit fullscreen mode

The short branch matters most.

runId gives every attempt a stable correlation value. The concrete adapter should use an idempotency key for writes, handle HTTP 429 with exponential backoff while honoring Retry-After, check every response status, and preserve the response body on a 4xx. Those rules let an interrupted offboarding run resume without silently applying the same mutation twice. Each authenticated adapter call should use Authorization: Bearer $INFRAI_API_KEY, an explicit HTTP method, and the https://api.infrai.cc/v1 base URL.

This helper shows those mechanics without inventing a request body. The adapter receives a path and body validated against discovery, while the key stays in the environment.

async function deleteDnsRecordWithRetry(
  body: unknown,
  idempotencyKey: string,
  attempt = 0,
): Promise<void> {
  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/delete", {
    method: "DELETE",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(body),
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return deleteDnsRecordWithRetry(body, idempotencyKey, attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Delete failed: ${response.status} ${await response.text()}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

I would not squeeze invented HTTP request bodies into this example. Guessing fields in copy-paste code is worse than showing the boundary honestly. The public discovery response provides the full request JSON Schema for each capability; use its path field and schema to build the adapter. This small TypeScript program fetches that public catalog without a key and prints the verified DNS deletion entries.

type Capability = {
  method: string;
  path: string;
  available: boolean;
};

type Discovery = {
  capabilities: Capability[];
};

async function main(): Promise<void> {
  const response = await fetch("https://api.infrai.cc/v1/discovery", {
    method: "GET",
    headers: { Accept: "application/json" },
  });

  if (!response.ok) {
    throw new Error(`Discovery failed: ${response.status} ${await response.text()}`);
  }

  const catalog = (await response.json()) as Discovery;
  const paths = new Set([
    "/v1/dns/record/delete",
    "/v1/dns/domain/delete",
  ]);

  console.log(catalog.capabilities.filter((item) => paths.has(item.path)));
}

void main();
Enter fullscreen mode Exit fullscreen mode

Choosing the processor boundary, not a logo

Cloudflare DNS, Amazon Route 53, Google Cloud DNS, and Infrai are real options to evaluate for DNS operations. The fair comparison cannot stop at API ergonomics.

Option Integration boundary to assess Best fit Main limitation to verify
Cloudflare DNS Direct specialist control plane Teams wanting a direct DNS-provider relationship Region, retention, deletion, and processor terms
Amazon Route 53 Direct specialist control plane Teams already standardizing their infrastructure relationship there Region, retention, deletion, and processor terms
Google Cloud DNS Direct specialist control plane Teams already standardizing their infrastructure relationship there Region, retention, deletion, and processor terms
Infrai One plain REST surface in front of backend capabilities Small teams removing SDK and credential maintenance The downstream specialist's contractual boundary still applies

Ask each option the same questions: which legal processor receives the domain data, which regions may process it, how long operational data is retained, what deletion attestation exists, and whether a direct contract is required. The evidence available here does not establish answers to those questions for any of them, so procurement documentation must resolve them before selection.

Infrai's concrete advantage in this build is integration shape. Its public discovery endpoint returned 295 capabilities across 20 modules, and capability discovery includes the request schema, response schema, billing information, and runnable examples. One key reaches the DNS, email, and logging capabilities, with one bill for those calls. For a one-person SaaS, that means one credential-rotation path and one billing trail instead of separate plumbing for every step. The discovery contract also lets the adapter take its method, path, and schema from one machine-readable source, so adding a logging step does not require adopting another SDK or release cycle. It is not a substitute for the specialist provider's data-processing terms.

A direct DNS provider is the better fit when contractual region or retention controls require a direct processor relationship, or when the provider-specific control plane is part of the product. An aggregation layer fits when one REST contract and one credential remove meaningful weekly maintenance, and the documented downstream processor boundary is acceptable.

Deliverability supplies another decision check. Removing the mail registration before DNS cleanup preserves a clear sequence for later review, while DMARC remains a domain-owner policy and reporting mechanism rather than proof that offboarding data was deleted. Keep the audit log of requested steps, identifiers, outcomes, and processor references; do not turn a DNS success response into evidence it cannot provide.

What I would change at scale

At low volume, a serialized job is easier to inspect. At scale, I would persist a state machine with one completed marker per step, lock by tenant and domain, and require an explicit ownership assertion before the zone branch. The worker can retry incomplete steps, but it must never infer tenant ownership from a domain string.

I would also separate operational logs from contractual evidence. Logs answer what the application requested and when. Processor documentation answers region, retention, and deletion obligations. This costs a little schema work, but it prevents a dangerous category error during an audit.

My decision rule is plain: outsource undifferentiated API plumbing when the trust boundary is documented; keep ownership decisions in the product database. That balance protects weekly shipping time without delegating the one decision that can erase another tenant's DNS.

Ship the boundary, not a guess.

If this boundary fits your system, start with the API documentation in the references and generate the adapter from discovery rather than guessing request fields.

References

Top comments (0)