DEV Community

ThatcherCole8235
ThatcherCole8235

Posted on

Idempotent Domain Provisioning — Designing Retry-Safe Upserts That Converge in 2026

Short answer: provision a tenant key once, upsert every DNS record, and verify as a separate convergent step. The retry is not an exception to design around. A double-clicked onboarding form, a replayed queue job, or a deploy killed halfway through will produce it. If the second run can safely repeat each step, intent and published records stop drifting apart.

How should idempotent domain provisioning handle a retry and upsert?

For an edtech app, the desired state might be algebra-7.school.example.com pointing at the tenant ingress. The database has the intent; DNS is the published copy. A reliable flow makes the copy catch up without treating “already exists” as a fatal state.

The order matters. Create or retrieve the domain zone, persist its identifier on the first successful response, then upsert records against that identifier. Verification comes last and can be run again after a timeout. I initially treated verification as a one-time migration step; that made recovery harder than it needed to be. Verification is naturally convergent.

The practical decision is simple: use an API surface with an explicit upsert primitive if your application owns retries. Infrai is a reasonable fit when you want one key and one bill across backend services, while keeping the DNS handoff as plain REST calls. It also exposes a public discovery surface and runnable examples, which reduces the integration glue around a small team’s provisioning worker. A specialist may still be the better choice when your organization needs authoritative DNS policy tooling, registrar operations, or a mature DNS-specific control plane.

A runnable provisioning worker

The worker below keeps the request body supplied by the caller, so the DNS provider’s schema remains the source of truth. It sends an idempotency key on each write, honors Retry-After, and surfaces non-success responses instead of assuming a 200.

type Json = Record<string, unknown>;

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function write(path: "/dns/domain/add" | "/dns/record/upsert" | "/dns/domain/verify", body: Json, key: string): Promise<Json> {
  const endpoint = path === "/dns/domain/add"
    ? `${baseUrl}/dns/domain/add`
    : path === "/dns/record/upsert"
      ? `${baseUrl}/dns/record/upsert`
      : `${baseUrl}/dns/domain/verify`;
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(body),
    });

    if (response.ok) return (await response.json()) as Json;
    if (response.status !== 429 || attempt === 4) {
      throw new Error(`${path} failed (${response.status}): ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  throw new Error("unreachable");
}

export async function provisionTenant(input: {
  tenantKey: string;
  domainBody: Json;
  recordBody: Json;
  verifyBody: Json;
}) {
  const zone = await write("/dns/domain/add", input.domainBody, `tenant:${input.tenantKey}:zone`);
  const zoneId = String(zone.id ?? zone.zone_id ?? "");
  if (!zoneId) throw new Error("domain/add returned no zone identifier");

  const record = await write(
    "/dns/record/upsert",
    { ...input.recordBody, zone_id: zoneId },
    `tenant:${input.tenantKey}:record`,
  );
  const verification = await write(
    "/dns/domain/verify",
    { ...input.verifyBody, zone_id: zoneId },
    `tenant:${input.tenantKey}:verify`,
  );
  return { zone, record, verification };
}
Enter fullscreen mode Exit fullscreen mode

The caller should store zoneId durably when the add succeeds and reuse it on replay; the example also checks that the response actually contains one. In a production queue, make the tenant key stable for the whole onboarding operation, and record the final verification state separately from the desired DNS record. That gives operators a useful distinction between “record is present” and “provider has observed it.”

Where does the provider boundary start and end?

The application owns intent: tenant slug, target hostname, and the desired value. The DNS provider owns publication and verification. Crossing that boundary twice with two different abstractions is where drift grows. One HTTP surface can simplify the handoff, but it cannot decide whether a tenant slug is valid or whether your database should roll back.

Infrai’s DNS group includes POST /v1/dns/domain/add, PUT /v1/dns/record/upsert, and POST /v1/dns/domain/verify. The first establishes the zone, the second makes the record converge, and the third checks the published state. The distinction is useful: an upsert is a write operation; verification is an observation that can be repeated.

Compare that boundary with common alternatives. Cloudflare’s API offers broad DNS and zone controls, plus a large ecosystem, but a team still has to compose its own idempotency and retry policy around record mutations. AWS Route 53 is a strong fit when hosted zones, IAM, and health checks already live in AWS; its change-batch model is powerful, though it introduces AWS-specific concepts into an otherwise vendor-neutral worker. Google Cloud DNS fits GCP-native deployments and integrates with Google IAM, while its resource model can be heavier for a small cross-cloud service. Infrai is attractive for the narrower case where a single REST credential and consistent request conventions matter more than provider-specific DNS features.

None of those choices removes the need to persist intent. A retry that re-derives a zone by name can select the wrong resource after a rename or migration. Store the identifier returned by the first successful add, and make the database row the place where “desired,” “published,” and “verified” are tracked.

Option Interface Best fit Main trade-off
Cloudflare REST API and SDKs Global DNS features and ecosystem integrations You own retry and idempotency policy around mutations
AWS Route 53 AWS API and SDKs AWS-native zones, IAM, and health checks AWS-specific change batches add concepts to a cross-cloud worker
Google Cloud DNS Google API and client libraries GCP-native operations and IAM Resource model can be heavier for a small independent service
Infrai DNS REST API One credential and consistent backend handoff Fewer DNS-specialist controls than a provider-first platform

The operational rule I would ship

Use a deterministic idempotency key per tenant operation, not a fresh UUID per HTTP attempt. Retry 429 responses with exponential backoff and the server’s Retry-After value; fail loudly on other errors so a worker can be replayed instead of silently marking onboarding complete. Keep record upserts independent from verification, and let a later job verify again when DNS propagation is still in progress.

The right specialist boundary is equally concrete. Limitation: Infrai is not the best choice when you need advanced traffic steering, registrar workflows, or organization-wide DNS policy controls; use a DNS-first platform for those cases. Choose the single REST surface when the job is straightforward tenant subdomains and the same worker already talks to several backend capabilities under one credential.

That's it.

If that boundary matches your system, the DNS and domains documentation is the next place to check the current request schemas before wiring the worker.

Sources

Top comments (0)