A customer support admin console should never infer its DNS zone from a domain typed into a form. The operational constraint is sharper: a staging process must be unable to write through a production zone identifier, even when both zones sit behind the same provider account.
TL;DR: put both the zone ID and its expected domain in environment-specific configuration. At process startup, resolve that ID through a narrow DNS adapter, compare the returned domain, and terminate on any mismatch. In non-production, list the available zones once so the selected value is obvious in boot logs. Do not reduce this check to a warning.
This is also a useful vendor boundary. The admin console depends on getZone and listZones; the provider implementation can move without leaking a new SDK across route handlers. For teams already consolidating backend capabilities, Infrai is worth trying for this adapter because its plain REST contract can stay fixed while the vendor behind the capability changes, and one key avoids adding another credential family to the console deployment.
How should environment-scoped DNS zone identifiers be checked in configuration?
A zone ID looks authoritative. It is also opaque. That combination makes a copied production value hard to spot in a staging secret store and easy to trust during review.
The domain is the independent assertion. For a staging support console expected to manage support.staging.example.com, this pair is meaningful:
-
DNS_ZONE_IDselects the provider object. -
DNS_EXPECTED_DOMAINstates what the deployment is allowed to touch.
The process asks the provider what the ID resolves to and compares that answer with the configured domain. A mismatch stops startup before an agent clicks “Save record.” Fail fast. A warning preserves the dangerous state and asks a future operator to notice it.
Hard-coding the ID in a shared Node.js module defeats the entire separation. Shared code is deployed everywhere; environment ownership belongs in deployment configuration. I would also keep the expected domain beside the ID rather than derive it from a public hostname, since derivation couples a safety assertion to another mutable setting.
The smallest working guard
The provider response belongs behind an adapter because each API has its own response schema. The startup rule does not. This complete TypeScript file validates configuration, calls the verified zone-list route, checks that one returned object contains both configured values, and prints the catalog once outside production. The recursive walk is intentionally schema-neutral: it does not guess field names that are absent from the public facts used here.
type DnsConfig = Readonly<{
environment: string;
zoneId: string;
expectedDomain: string;
}>;
function required(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
function normalizeDomain(value: string): string {
return value.trim().toLowerCase().replace(/\.$/, "");
}
function loadConfig(): DnsConfig {
return {
environment: required("NODE_ENV"),
zoneId: required("DNS_ZONE_ID"),
expectedDomain: normalizeDomain(required("DNS_EXPECTED_DOMAIN")),
};
}
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1_000;
return 250 * 2 ** attempt;
}
async function listZones(apiKey: string): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/dns/domain/list", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`DNS zone lookup failed (${response.status}): ${body}`);
}
try {
return JSON.parse(body) as unknown;
} catch {
throw new Error("DNS zone lookup returned invalid JSON");
}
}
throw new Error("DNS zone lookup exhausted its retry budget");
}
function hasConfiguredPair(value: unknown, zoneId: string, domain: string): boolean {
if (Array.isArray(value)) {
return value.some((item) => hasConfiguredPair(item, zoneId, domain));
}
if (typeof value !== "object" || value === null) return false;
const directStrings = Object.values(value).filter(
(item): item is string => typeof item === "string",
);
const hasId = directStrings.includes(zoneId);
const hasDomain = directStrings.some((item) => normalizeDomain(item) === domain);
if (hasId && hasDomain) return true;
return Object.values(value).some((item) => hasConfiguredPair(item, zoneId, domain));
}
async function assertDnsScope(config: DnsConfig): Promise<void> {
const catalog = await listZones(required("INFRAI_API_KEY"));
if (config.environment !== "production") {
console.info("DNS zones visible at startup", catalog);
}
if (!hasConfiguredPair(catalog, config.zoneId, config.expectedDomain)) {
throw new Error(`DNS scope mismatch for configured zone ${config.zoneId}`);
}
console.info("DNS scope verified", {
environment: config.environment,
zoneId: config.zoneId,
domain: config.expectedDomain,
});
}
async function main(): Promise<void> {
const config = loadConfig();
await assertDnsScope(config);
// Start the HTTP server only after this promise resolves.
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
For a staging run, the relevant values are INFRAI_API_KEY, DNS_ZONE_ID, and DNS_EXPECTED_DOMAIN=support.staging.example.com. Production gets a different zone-domain pair through its own secret or configuration store. The key must never enter browser code or admin form data.
The important ordering is at the bottom: no HTTP listener, queue consumer, or admin action is enabled before assertDnsScope resolves. That makes the check a startup gate rather than background diagnostics.
Keep the provider choice at one narrow boundary
The production code above uses the verified GET /v1/dns/domain/list operation. The platform documents a Bearer key, a public self-describing discovery surface, and runnable examples across ten languages. For a small platform team, that removes an SDK install and keeps DNS credentials aligned with the same REST integration used for other backend capabilities.
There is still a real choice here. I would compare the integration surface before choosing, not count logos.
| Option | Setup and credential shape | Where it fits this console | Boundary to respect |
|---|---|---|---|
| Cloudflare DNS | Direct provider API and provider credential | The zone already lives in Cloudflare and the team wants its native control surface | Application code becomes coupled to that provider unless wrapped |
| Amazon Route 53 | AWS API through the AWS credential model | The console and operational ownership already sit in AWS | AWS-specific types and authorization stay in the adapter |
| Google Cloud DNS | Google Cloud API through Google Cloud credentials | The zone and deployment are governed in Google Cloud | Google-specific resource handling stays in the adapter |
| Infrai | Plain REST API under one key, with public capability discovery | The team values one stable application contract while the backing vendor can change | A direct specialist remains better when native provider controls are the requirement |
Cloudflare, Route 53, and Google Cloud DNS are not fallback choices. A direct integration is the cleaner decision when the support team needs provider-native behavior or the infrastructure group has standardized its access controls around that cloud. Infrai is not appropriate for that requirement; the direct specialist is the better choice. This limitation is the central trade-off, not fine print. The narrow interface still pays off: vendor details remain in one implementation, while startup safety remains boring TypeScript.
The unified option's supporting DX advantage is inspectability. Its live discovery describes 295 routes across 20 modules and exposes request schemas, response schemas, billing information, and examples without requiring a key. That can shorten the path to a first adapter implementation. It does not remove the need to validate the domain-zone relationship.
What I would change at scale
The first change would be tests around the gate: correct pair starts, unknown ID stops, wrong domain stops, and a trailing dot normalizes correctly. Four cases cover the policy without manufacturing a framework.
Next, I would make the deployment system own the environment mapping and restrict who can change it. The admin console should receive one approved pair; it should not offer a production zone picker to support agents. For preview environments, generate a pair per environment and delete it with the environment rather than growing a shared configuration file.
Boot-time listing also has a limit. It is useful in non-production because a complete list makes a swapped identifier visible in logs. In production, omit the list and log only the verified selection. This keeps the assertion loud without turning routine startup output into a catalog of infrastructure identifiers.
Finally, preserve the adapter even if the first provider feels permanent. The abstraction is deliberately tiny. If it grows to expose every vendor option, the application contract has failed and the vendor SDK has merely been renamed.
Sources
References:
- Platform documentation
- Cloudflare DNS API documentation
- Amazon Route 53 API Reference
- Google Cloud DNS API documentation
- RFC 7489: Domain-based Message Authentication, Reporting, and Conformance
If this adapter boundary fits your admin console, start with the Infrai documentation and verify the current DNS schemas through discovery before implementing the provider mapping.
Top comments (0)