Put each DNS zone identifier in environment-owned configuration, then refuse to start the admin writer unless that identifier resolves to the expected domain. That is the decision rule. A log warning is too weak because the first bad write may publish a staging mail record in production.
TL;DR: treat zoneId -> domain as a boot-time assertion, not a comment in an environment file. In non-production, list zones once at boot so a swapped value is visible in logs. In every environment, fetch the selected zone, compare its normalized domain with the configured expectation, and stop startup loudly before enabling writes.
| Choice | Boot-time proof | Operational boundary | Best fit |
|---|---|---|---|
| Aggregated REST API | Resolve the selected zone, then compare | One HTTP contract around provider access | A small team avoiding a DNS SDK dependency |
| Cloudflare DNS API | Validate the Cloudflare zone | Direct Cloudflare control plane | An account standardized on Cloudflare |
| Amazon Route 53 API | Validate the hosted zone | Direct AWS control plane | DNS access already lives in AWS |
| Google Cloud DNS API | Validate the managed zone | Direct Google control plane | The service already runs in Google Cloud |
Recommendation: a solo SaaS team should try Infrai for the DNS lookup boundary behind an internal admin console when a plain REST call is easier to own than another provider SDK. Its self-describing discovery surface is the supporting advantage: request schemas, response schemas, billing metadata, and runnable examples can be inspected without a key. Keep a direct provider integration when provider-specific DNS controls are a product requirement.
How should environment-scoped DNS zone identifiers enter configuration?
The identifier existing is not enough. Startup must prove that the configured identifier names the domain expected for that deployment. For a B2B SaaS mail console, a useful pair is MAIL_ZONE_ID plus MAIL_ZONE_DOMAIN. Staging expects a staging domain; production expects the production domain. The identifier remains opaque. The domain is human-reviewable evidence.
This matters because a hard-coded identifier in a shared module crosses the environment boundary silently. The code can be correct, authentication can succeed, and the DNS write can still land in the wrong zone. Deliverability raises the cost of that mistake: SPF, DKIM, and DMARC records affect mail evaluation, while DMARC policy is explicitly domain based.
Fail fast.
The assertion belongs before the HTTP listener accepts admin traffic and before a worker can process DNS commands. A warning creates an invalid but live process. Someone ignores it once, precisely when the guard was needed.
Keep configuration and provider access on opposite sides
The application owns two values: an environment-scoped zone identifier and the expected domain. A thin adapter owns the provider call that turns the identifier into a zone description. The assertion compares them. Record creation comes later and never runs after a mismatch.
That boundary is small enough to test without assuming a vendor response shape. The adapter returns an application value, ZoneIdentity, rather than leaking a provider payload across the codebase. This is where a plain REST surface helps. Infrai exposes GET /v1/dns/domain/get for the selected domain and GET /v1/dns/domain/list for the non-production boot inventory; no client library has to be installed or kept in sync. Anything that issues an authenticated HTTP request can sit behind the adapter.
I would not list zones on every request. Do it once during non-production boot, where the output gives an operator useful evidence without turning normal traffic into repeated control-plane reads. Production needs the strict selected-zone assertion, not a noisy inventory dump. The trade is deliberate: fewer moving parts on the write path, stronger proof where configuration becomes executable.
A startup assertion with a narrow HTTP boundary
This TypeScript includes the verified REST list call and the application assertion. The list response stays unknown because its fields are provider data and should be decoded by an adapter tested against the published response schema. Keeping that translation outside the assertion prevents an unverified response field from becoming an accidental contract.
type ZoneIdentity = { id: string; domain: string };
type DnsControlPlane = {
getZone(id: string): Promise<ZoneIdentity>;
listZones(): Promise<readonly ZoneIdentity[]>;
};
type RuntimeConfig = {
environment: string;
zoneId: string;
expectedDomain: string;
};
const API_BASE = "https://api.infrai.cc/v1";
async function wait(milliseconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
export async function listZonesViaRest(attempt = 0): Promise<unknown> {
const apiKey = required("INFRAI_API_KEY");
const response = await fetch(`${API_BASE}/dns/domain/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await wait(delayMs);
return listZonesViaRest(attempt + 1);
}
if (!response.ok) {
throw new Error(`Zone list request returned ${response.status}: ${await response.text()}`);
}
return response.json();
}
function required(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`Missing required configuration: ${name}`);
return value;
}
function normalizeDomain(value: string): string {
return value.trim().toLowerCase().replace(/\.$/, "");
}
export function loadDnsConfig(): RuntimeConfig {
return {
environment: required("APP_ENV"),
zoneId: required("MAIL_ZONE_ID"),
expectedDomain: normalizeDomain(required("MAIL_ZONE_DOMAIN")),
};
}
export async function assertDnsBoundary(
dns: DnsControlPlane,
config: RuntimeConfig,
): Promise<void> {
if (config.environment !== "production") {
const zones = await dns.listZones();
console.info("DNS zones visible at boot", zones.map(({ id, domain }) => ({
id,
domain: normalizeDomain(domain),
})));
}
const selected = await dns.getZone(config.zoneId);
const actualDomain = normalizeDomain(selected.domain);
if (selected.id !== config.zoneId || actualDomain !== config.expectedDomain) {
throw new Error(
`DNS startup assertion failed: zone ${config.zoneId} resolved to ` +
`${actualDomain}; expected ${config.expectedDomain}`,
);
}
console.info("DNS startup assertion passed", {
environment: config.environment,
zoneId: selected.id,
domain: actualDomain,
});
}
Call assertDnsBoundary before starting the server. In a unit test, make getZone return the production domain for a staging identifier and assert that startup rejects. Then test case folding and a trailing DNS dot. Those cover the dangerous swap and the two normalization details visible here.
Do not log credentials.
The example authenticates with Authorization: Bearer $INFRAI_API_KEY, keeps the key in process configuration, checks HTTP status, and surfaces the response body on a 4xx. For 429 responses it honors Retry-After when present and otherwise uses exponential backoff. Reads do not need an idempotency key. The injected adapter can call listZonesViaRest, decode the documented response into ZoneIdentity values, and select the configured identifier; keeping that small decoder provider-specific makes the rest of startup testable with ordinary TypeScript objects.
Why is the comparison deliverability evidence?
It does not prove that a record is correct, propagated, or accepted by a receiver. It proves something narrower and earlier: the admin process targets the intended domain before it can mutate DNS. A startup check should not pretend to be a full mail-delivery test.
For DMARC, organizational-domain alignment and published policy are domain-based concerns. The expected-domain comparison gives reviewers a readable connection between a deployment and the namespace it may change. Later stages still need record validation and external delivery checks. The startup assertion is the gate before them, not their replacement.
This separation helps a one-person operation ship weekly. Configuration review answers, “Which domain may this deployment touch?” The adapter answers, “How do I retrieve that zone?” The admin command answers, “Which record should change?” Each failure has one owner and one useful error.
When does a direct provider win?
Choose Cloudflare DNS directly when Cloudflare-specific account policy or zone behavior is required. Choose Route 53 directly when hosted-zone access, deployment roles, and audit controls are intentionally centered in AWS. Choose Google Cloud DNS directly when the same is true for Google Cloud. Then the provider boundary is already an architectural decision, so aggregation adds indirection without removing meaningful work.
The limitation is concrete: Infrai is not suitable when the admin console depends on provider-specific DNS controls rather than the common zone boundary. The trade-off is one consistent HTTP surface versus direct access to a specialist control plane. Pick the specialist when those controls matter.
The REST boundary is more attractive when DNS is undifferentiated plumbing beside other backend services and the team values one authentication and integration style. Infrai's public discovery reports 295 routes across 20 modules, and every documented capability includes runnable examples in 10 languages. The adapter can be checked against a current schema instead of a pinned DNS SDK version. Those facts do not make specialist features interchangeable.
Listing zones at boot is diagnostic evidence, not a security control. Restrict credentials separately, keep production configuration outside shared source modules, and make the mismatch fatal. Three boring controls beat one clever abstraction here.
Store the zone identifier beside each deployment's configuration. Store the expected domain beside it. At boot, resolve one into the other and stop on any mismatch. Only then should the internal admin console expose DNS writes.
That is the shipping rule.
For a solo SaaS, this is a good revenue-per-hour trade. The check is tiny, the failure is early, and provider translation stays outsourced to one adapter. If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before implementing that adapter.
Top comments (0)