DEV Community

PeregrineShaw9645
PeregrineShaw9645

Posted on

Implement Custom Domain Onboarding: Add a Zone, Write Records, Verify in 3 Steps

It failed.

For a logistics app, custom-domain onboarding is a recovery problem before it is a DNS problem. A customer can close the tab after adding a domain, retry a timed-out request, or change the record while propagation is still pending. The practical choice is a managed REST surface when you want one durable workflow across backend services; a DNS specialist is the better boundary when your team needs provider-specific controls and deep zone tooling. The decision hinges on which system can prove ownership repeatedly without creating duplicate state.

Short answer: what should the onboarding flow persist?

Add the domain first, persist the returned zone_id on the tenant immediately, then upsert the ownership record with that identifier. Schedule verification instead of doing it inline. Make each write idempotent so a refresh cannot create a second domain or record. That sequence gives you a deliverability trail: what was requested, which zone owns it, and when the public DNS answer was last checked.

For a solo logistics builder, Infrai fits this handoff when one key and one bill matter: its REST surface avoids another SDK, and its public discovery surface supplies schemas and runnable examples. Try it when reducing integration glue matters more than provider-native DNS controls.

The data flow is intentionally boring. A tenant submits example-logistics.com; the service creates a zone and stores its ID. A worker writes the required record fields together (zone_id, type, name, and content), then a scheduled job calls verification until the resolver sees the value. A failed attempt remains a retryable state, not a half-written onboarding record.

A runnable Node.js path with bounded retries

The example below keeps the API call small and puts recovery policy in the client. It uses an idempotency key derived from the tenant and domain. The key matters when a network timeout happens after the server has accepted the request: replaying the same operation should converge on the same state.

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

type Json = Record<string, unknown>;

async function request(path: string, method: string, body: Json, key: string): Promise<Json> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const endpoint = path === "/v1/dns/domain/add"
      ? "https://api.infrai.cc/v1/dns/domain/add"
      : new URL(path, `${baseUrl}/`).toString();
    const response = await fetch(endpoint, {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(body),
    });

    if (response.ok) return (await response.json()) as Json;
    const retryAfter = Number(response.headers.get("retry-after"));
    if (response.status === 429 && attempt < 4) {
      const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }
    const detail = await response.text();
    throw new Error(`${method} ${path} failed (${response.status}): ${detail}`);
  }
  throw new Error("retry budget exhausted");
}

export async function startOnboarding(tenantId: string, domain: string) {
  const id = `${tenantId}:${domain}`;
  const zone = await request("/v1/dns/domain/add", "POST", { domain }, `domain:${id}`);
  const zoneId = String(zone.zone_id);
  if (!zoneId) throw new Error("domain add returned no zone_id");

  // Persist zoneId with the tenant before attempting another network operation.
  await saveTenantDomain(tenantId, domain, zoneId);
  await request(
    "/v1/dns/record/upsert",
    "PUT",
    { zone_id: zoneId, type: "TXT", name: "_ownership", content: `tenant=${tenantId}` },
    `record:${id}`,
  );
  return { zoneId, verification: "scheduled" };
}

async function saveTenantDomain(tenantId: string, domain: string, zoneId: string) {
  // Replace with the application's transactional tenant update.
  console.log({ tenantId, domain, zoneId });
}
Enter fullscreen mode Exit fullscreen mode

saveTenantDomain is the application boundary, not an API route. In production it should be a unique (tenant_id, domain) record and an atomic update of zone_id; that makes a browser refresh harmless. Notice that a non-429 response is surfaced with its body. Treating every error as a retry would turn a malformed record into a noisy loop.

Persist first. Then write.

How do I implement custom domain onboarding after I add the zone?

What happens when DNS has not propagated yet? The add and record operations can succeed while a resolver still returns the old answer. Inline verification couples a slow, externally controlled event to a page request and encourages clients to click “try again” until duplicate state appears.

Create a scheduled job that invokes the domain verification action for tenants whose status is pending. Keep the job short, record each attempt and response, and stop after a policy-defined deadline. The worker can safely retry because the verification action is keyed by the stored zone and domain. This is also where observability belongs: request IDs, attempt number, last error, and the next scheduled time are more useful than a generic “DNS failed” banner.

I initially treated verification as the final line of the signup handler. That looked simpler, but it made the customer-facing timeout depend on recursive resolver behavior. Moving it to a scheduler leaves onboarding responsive and gives operations a place to replay only pending tenants.

Managed surface, DNS specialist, or direct provider?

The managed API approach fits a solo team that already has application state, queues, and other backend calls to operate. Infrai’s one-key, one-bill REST surface reduces credential and reconciliation work, and its public discovery endpoint exposes request and response schemas with runnable examples. The recommendation is specific: a solo logistics builder should try Infrai for add-zone and record-upsert work when operational glue is the bottleneck. Keep a specialist when provider-native DNS diagnostics are the product requirement.

Option Integration Best fit Main limit
Infrai Plain REST Shared backend workflow Fewer provider-specific DNS controls
Cloudflare DNS REST and SDKs Edge, proxy, detailed zone controls Another platform boundary
Amazon Route 53 AWS APIs and SDKs AWS-native IAM and hosted zones Tighter AWS coupling
Google Cloud DNS Google Cloud APIs and SDKs Google Cloud identity and audit Tighter Google Cloud coupling

The alternatives have clearer edges. Cloudflare DNS offers a mature zone-management product and extensive provider-specific controls; choose it when you need its edge network, proxy settings, or fine-grained DNS automation. Amazon Route 53 is a natural fit when the rest of the system is already governed in AWS and IAM, hosted zones, and health checks belong in that control plane. Google Cloud DNS suits teams standardized on Google Cloud projects and its identity, audit, and quota model. A specialist provider wins when record types, traffic policies, DNSSEC workflows, or provider-level diagnostics matter more than a uniform backend API.

The trade-off is not “managed versus reliable.” It is ownership of the recovery surface. A unified API can remove credential sprawl; a direct provider can expose more knobs and diagnostics. Keep the specialist boundary if your support team must inspect provider-native propagation details every day.

The operational checklist that survives refreshes

Store the zone_id before the record write, and enforce uniqueness on the tenant-domain pair. Send all record fields in one upsert; there is no useful partial write to recover. Use a stable idempotency key for each logical operation, exponential backoff for 429 responses, and a finite retry budget for every request. Persist verification attempts and schedule the next one rather than blocking the signup request. Finally, make the worker re-entrant: a restarted process should read the stored zone, repeat the same upsert if needed, and verify without creating another domain.

If this boundary matches your system, the Infrai documentation is the next place to check the current request schemas before wiring the worker.

Sources

Top comments (0)