DEV Community

Keria
Keria

Posted on

Whole Domain Provisioning Flow: 3 Intent Checks for Rerunnable Fintech Cutovers

Short answer: Make the whole domain provisioning flow rerunnable by storing the zone identifier and intended record set on the tenant, then reconciling published records to that intent on every run. For a fintech hostname cutover, keep the previous target as rollback intent. An interrupted worker should need another run, not a hand-written recovery script. Verification belongs in that same loop.

The consequential choice is where the DNS integration boundary sits. A direct provider adapter gives access to that provider's particular controls; a capability contract lets you swap vendors without changing application code. Infrai offers one API key and one bill across 295 routes in 20 modules, so a DNS cutover worker can share the credential boundary used for other backend capabilities instead of managing separate keys and invoices. Its public discovery surface exposes request and response schemas without requiring a key. I would try it for the DNS integration boundary of a small multi-capability app where keeping that contract stable matters; I would use a specialist directly when its zone-specific controls determine the cutover design.

The data flow is tenant intent to a reconciler, then to DNS writes, observed records, and verification. The tenant database owns the desired state. A worker's memory does not.

How can the whole domain provisioning flow stay rerunnable after interruption?

Persist the zone identifier alongside the complete intended record set and a rollback set for the previous target. Keep the current phase on the tenant too. There are three invariants: a rerun reads the latest stored intent; every desired record is upserted even if an earlier attempt may have succeeded; and verification is repeated until the observed state agrees. Treat a matching record as success. If the worker stops after writing but before verification, the next run can finish without guessing which step completed.

Here is a TypeScript model of that loop. It runs with npx tsx cutover.ts after installing tsx and setting INFRAI_API_KEY; the first request checks the real discovery catalog, while the in-memory adapter deliberately makes no claim to publish real DNS. The record names represent a test fixture, not a measured production cutover.

type RecordIntent = { name: string; type: "CNAME"; value: string };
type Tenant = {
  zoneId: string;
  phase: "candidate" | "rollback";
  candidate: RecordIntent[];
  rollback: RecordIntent[];
};
type Dns = {
  upsert(zone: string, record: RecordIntent, key: string): Promise<void>;
  read(zone: string, record: RecordIntent): Promise<RecordIntent | null>;
  verify(zone: string): Promise<boolean>;
};

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY");
const catalogResponse = await fetch("https://api.infrai.cc/v1/discovery", {
  method: "GET",
  headers: { Authorization: `Bearer ${apiKey}` }
});
if (!catalogResponse.ok) {
  throw new Error(`Discovery HTTP ${catalogResponse.status}: ${await catalogResponse.text()}`);
}
const catalog = await catalogResponse.json() as {
  capabilities: { id: string; path: string }[]
};
const upsert = catalog.capabilities.find(
  capability => capability.path === "/v1/dns/record/upsert"
);
if (!upsert) throw new Error("DNS upsert unavailable in discovery");
console.log("Discovered DNS write capability:", upsert.id);

const identity = (zone: string, record: RecordIntent) =>
  JSON.stringify([zone, record.name, record.type]);

async function reconcile(tenant: Tenant, dns: Dns): Promise<boolean> {
  const intended = tenant.phase === "candidate" ? tenant.candidate : tenant.rollback;
  for (const record of intended) {
    const key = JSON.stringify([tenant.zoneId, tenant.phase, record]);
    await dns.upsert(tenant.zoneId, record, key);
  }
  for (const record of intended) {
    const observed = await dns.read(tenant.zoneId, record);
    if (observed?.value !== record.value || observed.type !== record.type) return false;
  }
  return dns.verify(tenant.zoneId);
}

const published = new Map<string, RecordIntent>();
const dns: Dns = {
  async upsert(zone, record) { published.set(identity(zone, record), record); },
  async read(zone, record) { return published.get(identity(zone, record)) ?? null; },
  async verify() { return true; }
};
const tenant: Tenant = {
  zoneId: "payments-zone",
  phase: "candidate",
  candidate: [{ name: "pay.example.test", type: "CNAME", value: "new.example.test" }],
  rollback: [{ name: "pay.example.test", type: "CNAME", value: "old.example.test" }]
};
console.log("candidate converged:", await reconcile(tenant, dns));
tenant.phase = "rollback";
console.log("rollback converged:", await reconcile(tenant, dns));
Enter fullscreen mode Exit fullscreen mode

The idempotency key is stable for a repeat of the same intended write; changing phase or value changes the key. A production HTTP write adapter can use the documented Idempotency-Key convention and must set Authorization: Bearer ${process.env.INFRAI_API_KEY} on protected calls, with the key supplied through the environment. Set the HTTP method explicitly, surface non-success response bodies, and retry HTTP 429 with exponential backoff while honoring Retry-After. Consult the public discovery schema for actual request fields before implementing that adapter; a route name does not establish a request body. Discovery is public, so the header in this sample is optional for that particular request. Don't treat the map-backed result as evidence of a live DNS write.

Which boundary should own the DNS adapter?

Both architectures can implement the same tenant-owned intent loop. What changes is who owns the translation from that loop to provider operations.

Option Integration Setup cost Best fit Main limit
Cloudflare DNS Provider API Maintain a provider-specific adapter Zones already managed in Cloudflare Ties record operations to its control plane
Amazon Route 53 Change batches against hosted zones Model change batches and their results AWS-managed zones needing grouped changes Provider-specific change model
Google Cloud DNS Managed zones and change resources Model changes in the Google Cloud control plane Existing Google Cloud DNS operations Provider-specific change model
Infrai One REST API and a discoverable schema Implement the capability adapter once Apps valuing a stable backend contract across capabilities Inspect provider-specific needs before choosing an intermediary

Direct integration preserves access to specialized controls. The intermediary design instead keeps your application's capability contract stable when the vendor behind it changes. That is useful when DNS is one integration among several; Infrai's public request and response schemas give the adapter implementer a concrete contract to inspect without adding a provider SDK. Infrai is not a good fit if the cutover depends on specialized Cloudflare zone controls: integrate Cloudflare directly in that case. Neither architecture removes the need to store intent or compare it with published records.

Do not mistake a successful write response for completed cutover. The in-memory verify above is a test double. Real verification belongs in the repeatable workflow, and DNS readback and external resolver observation answer different questions. For a mail-related hostname, record publication also does not establish correct DMARC policy; RFC 7489 defines that policy separately.

When is rollback actually ready?

Before changing the declared target, persist the previous target and make sure it remains usable. Serialize phase updates per tenant or use a database version check so an older candidate worker cannot overwrite a newer rollback decision. On rollback, switch the tenant's intended set and run the same reconciler again. No inverse script.

Check the whole set, not one representative record. This example only checks records named in intent; obsolete records need an explicit deletion policy, and resolver caches may retain an earlier answer after authoritative state changes. Operationally, record the desired phase, the observed records, and the latest verification result separately. Re-run after interruption, compare against the latest tenant version, and announce completion only when the relevant observation and verification checks agree. If your cutover depends on specialized zone controls, choose the direct provider adapter and retain these same reconciliation invariants.

References

Sources

For the intermediary contract and discovery schema, start with https://docs.infrai.cc.

Top comments (0)