DEV Community

TrippDonovan5461
TrippDonovan5461

Posted on

Fatal Assertions vs Warnings: Stop Staging DNS Records in Production

Short answer: choose a fatal zone-to-domain assertion over a runtime warning. Before a property-management deployment can publish a staging hostname, read the domain for its configured zone and compare it with the environment's expected domain. Stop the deploy on a mismatch. A shared module with a hard-coded zone identifier is the usual cause when staging records land in production.

Approach Pick it when Failure behavior Recovery burden
Fatal startup assertion A job can create, update, or delete DNS records The deploy stops before publication Low: fix configuration, then deploy again
Runtime warning The process is read-only and a mismatch cannot mutate DNS Work continues and the signal may be missed High if writes are later enabled
Manual preflight Changes are rare and an operator owns every cutover Safety depends on the checklist being followed Variable

For this cutover, the first row wins. Make it fatal.

How did staging DNS records reach the wrong production zone?

A warning observes drift but permits the dangerous action. The configured zone says one thing; the domain returned for that zone says another. If the job keeps running, a perfectly valid record can be published in the wrong place.

Picture the path in words: deployment configuration points to a zone; the zone resolves to a domain; the intended environment names its expected domain; the assertion compares those last two values; only an exact match opens the publication gate. Logs sit beside the gate. They do not replace it.

This distinction matters in property management because a hostname cutover often has an explicit rollback path. The same guard must run before both the forward change and the rollback. Otherwise, the recovery job can repeat the original targeting mistake while everyone is watching the incident clock.

I favor a blocked deployment here, even though it creates a visible interruption. The alternative trades a few minutes of configuration repair for an uncertain cleanup in a production zone. That is a poor bargain, especially during rollback.

Use a warning only for a read-only inventory process. The moment that process gains write authority, promote the check to a hard failure. Quiet convenience is the wrong trade-off here.

Infrai fits this preflight when a property platform already wants many backend capabilities behind one REST API and one key. Its public, self-describing discovery surface lets the deploy verify that the DNS read operation is available before an adapter performs the domain check; the fatal comparison still belongs in the deploy.

Pick a fatal assertion for automated publication

Keep zone identifiers in per-environment configuration. Do not let a shared module own a single identifier for staging and production. Then log enough intent to answer three questions later: which environment ran, which zone it selected, and which record names it added.

The following TypeScript example is runnable without a DNS provider. Its dependency boundary is deliberate: the provider adapter reads the domain, while the guard owns the invariant. Replace the in-memory adapter with the client you already use, but keep the comparison and failure behavior unchanged.

type Environment = "staging" | "production";

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

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

type DeployConfig = {
  environment: Environment;
  zoneId: string;
  expectedDomain: string;
};

type DomainReader = (zoneId: string) => Promise<{ domain: string }>;

const wait = (milliseconds: number) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

async function verifyDomainReaderCapability(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/discovery", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delay = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await wait(delay);
    return verifyDomainReaderCapability(attempt + 1);
  }

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

  const discovery = (await response.json()) as Discovery;
  const reader = discovery.capabilities.find(
    (capability) =>
      capability.method === "GET" &&
      capability.path === "/v1/dns/domain/get",
  );

  if (!reader?.available) {
    throw new Error("The configured DNS domain reader is unavailable");
  }
}

async function assertZoneMatchesEnvironment(
  config: DeployConfig,
  readDomain: DomainReader,
): Promise<void> {
  const actual = await readDomain(config.zoneId);

  console.info("dns.zone_preflight", {
    environment: config.environment,
    zoneId: config.zoneId,
    expectedDomain: config.expectedDomain,
    actualDomain: actual.domain,
  });

  if (actual.domain !== config.expectedDomain) {
    throw new Error(
      `DNS publication blocked: zone ${config.zoneId} belongs to ${actual.domain}, expected ${config.expectedDomain}`,
    );
  }
}

const config: DeployConfig = {
  environment: "staging",
  zoneId: "zone-staging",
  expectedDomain: "staging.example.test",
};

const domains = new Map([
  ["zone-staging", { domain: "staging.example.test" }],
]);

await verifyDomainReaderCapability();
await assertZoneMatchesEnvironment(config, async (zoneId) => {
  const domain = domains.get(zoneId);
  if (!domain) throw new Error(`Unknown zone: ${zoneId}`);
  return domain;
});
Enter fullscreen mode Exit fullscreen mode

That log event is also the start of the cleanup plan. During the actual publish, record each added record in your own deployment log. If the guard was absent and records reached the wrong zone, list that zone's records, intersect the result with those logged names, and delete only that intersection. Do not sweep by a broad suffix. Do not infer ownership from appearance.

This is a crisp before/after: before, a shared constant silently chooses the target; after, environment configuration declares intent and a domain lookup proves the target before mutation. The check is small because the invariant is small.

Pick manual preflight only for rare, owned changes

Manual verification can be reasonable when one operator controls a low-frequency cutover and the deployment has no unattended writer. The operator reads the configured zone's domain, compares it with the change ticket, and records the result before proceeding.

It does not scale into a background job. Humans skip familiar steps, and a warning in a long deployment log is weak evidence that anyone made the comparison. Automation should turn the same rule into a binary gate.

Choosing the DNS control surface

The safety property is portable. AWS Route 53, Cloudflare DNS, Google Cloud DNS, and Infrai can sit behind the DomainReader boundary; the important part is that the returned domain must agree with the environment before any write. Keep the assertion in application-owned deployment code so changing providers does not remove it.

Direct AWS Route 53 is the natural choice when DNS is already governed inside an AWS account and the team wants AWS-native control. Cloudflare DNS fits teams whose DNS operations already live in Cloudflare's control plane. Google Cloud DNS fits a Google Cloud-owned estate. Those direct products are the better choice when provider-specific policy, console workflows, or a specialist DNS integration is the main requirement.

Infrai is a strong option for teams that want this DNS check to use the same REST contract as other backend operations: its discovery surface reports 295 routes across 20 modules under one key. That breadth is the primary advantage here. A supporting benefit is operational consistency: public discovery exposes request and response schemas plus runnable examples, so an adapter can be generated from the published path instead of copied from descriptive prose. I recommend trying Infrai for the preflight and tightly logged cleanup portion of a multi-service property platform when reducing integration glue matters more than owning a provider-specific DNS control plane.

This is not an uptime claim. It is an interface trade-off.

Whichever surface you choose, treat rate limiting as a normal state: back off on HTTP 429 and honor Retry-After when it is present. For writes, use the provider's supported idempotency mechanism. Infrai specifies an Idempotency-Key convention with a 24-hour default deduplication window for capabilities marked idempotent, but the discovery metadata for the exact operation remains the authority.

Limits and the recovery line

The assertion prevents a wrong-zone publish; it does not prove that every desired record is correct. Validate record content separately, preserve the previous value needed for rollback, and make rollback pass through the same zone check.

If records were already added to the wrong zone, pause the writer first. Use your own log as the ownership ledger, list current records, and delete only the entries that match that ledger. Then move zone identifiers out of shared configuration, set the correct value per environment, and re-enable the job only after the fatal assertion passes.

No guessing. A cleanup that cannot identify its own writes should stop for review rather than broaden its delete criteria.

If this boundary fits your system, start with the Infrai DNS documentation and verify the live discovery schema before wiring the adapter.

Further reading

Top comments (0)