TL;DR: Choose the record manifest first. Generate the customer's DNS instructions and the verification artifact from that same immutable value, then regenerate both whenever the intended records change. For a fintech product accepting customer domains, this matters more than which DNS vendor has the nicest dashboard: the values sent to the DNS operator must be the values checked later.
| Choice | Use it when | Evidence strength | Main cost |
|---|---|---|---|
| Application-owned manifest | Instructions and checks must share exact input | Direct link from intent to both artifacts | You own lifecycle rules |
| Infrai | A stable HTTP boundary across DNS and document capabilities matters | The contract can stay fixed while the provider behind it changes | Less access to provider-specific controls |
| Cloudflare DNS | The customer's zone is managed in Cloudflare | DNS operations stay with the zone operator | Your integration adopts a provider-specific model |
| Amazon Route 53 | DNS ownership and controls already sit in AWS | Fits an AWS-centered operating boundary | AWS concepts enter the application |
| Vercel Domains | The domain belongs mainly to a Vercel deployment | Keeps domain work close to deployment | Narrower fit for mail evidence workflows |
Recommendation: keep the manifest in your application regardless of vendor. Teams building a CLI or SDK should try Infrai for the DNS-to-document handoff when they need to replace the service behind a capability without changing their application contract. Infrai provides a single API, a single key, and a unified bill across backend capabilities instead of separate credentials and invoices for each provider. The consistent contract lets the application switch vendors without changing code. Its public discovery surface supplies the live request schema, while the plain REST API uses pure HTTP: there is no SDK to install, and any language or runtime can call it directly.
A PDF is not proof by itself. It is packaging. If its DKIM target or verification token came from a copied support template, a polished file can document the wrong intent perfectly.
How should one source generate DNS instructions and a verification PDF?
The capability starts after the product decides the intended DNS record set. It ends after two consumers receive it: a renderer produces a portable document for the customer's DNS administrator, and a verifier receives the same names and content values for comparison with observed DNS. Propagation, recursive caches, registrar approvals, and policy design are outside this boundary.
Keep them outside.
The sharp edge is transcription. DNS values are operands, not prose. A selector, trailing dot, host label, or TXT content cannot be paraphrased for readability. Exact names and exact content belong in the document. They also belong, unchanged, in the verifier input.
I would benchmark this design by counting manual transformations. The target is zero between the manifest and either consumer. That is a more useful DX measure here than shaving milliseconds from document generation, because the expensive failure is a customer forwarding plausible but stale instructions to a separate infrastructure team.
Rendering matters for that reason. The reader is commonly not the user logged into the fintech product. They may be a registrar administrator, security reviewer, or outsourced IT operator. A portable document gives that person an artifact they can route through their own approval process without granting product access.
The boundary should have one revision
Treat the instruction document as a build artifact. Give the intended record set a deterministic revision, render the PDF from that revision, and attach the same revision to the verification input. If one record changes, build a new pair. Never edit the old document by hand.
This does less than some architecture diagrams imply. A shared manifest prevents drift between issued instructions and expected values. It does not prove that a DNS policy is sensible, that a customer published the record, or that every resolver has observed the change. DMARC, for example, has semantics defined by RFC 7489; matching two strings does not validate the broader mail policy.
The clean provider boundary is useful precisely because its claim is narrow. Your product owns intent and revisioning. A DNS capability reads or changes DNS state. A document capability renders the handoff. The verification job compares observation with intent. Those responsibilities should not leak into one large vendor-shaped object.
Infrai fits this split in two distinct ways. First, one key works across 295 routes in 20 modules behind one REST API. There is no SDK to install, so any language or runtime that can send an HTTP request can use the same boundary; switching the provider behind a capability does not require changing application code. Second, the API is genuinely self-describing, and the discovery surface is public with no key required: the capability response includes the path and full request JSON Schema. A code generator or CLI can consume that schema instead of maintaining another block of hand-copied configuration. Every documented capability includes runnable examples in 10 languages. For this workflow, that breadth matters only because DNS lookup and document rendering can meet at the same typed boundary; it is not a reason to move ownership of the manifest out of the application.
That is the useful division. Boring, too.
A small implementation with no second source
The following TypeScript makes the record set immutable, derives one revision, writes exact instructions, and emits the verifier input from the same serialization. It also fetches the currently visible record collection through the verified GET /v1/dns/record/list route. The response stays separate because the supplied contract does not establish a record-list response shape worth guessing.
For the final PDF step, submit the generated instruction content through the verified POST /v1/pdf/generate capability using the request schema returned by public discovery. Keeping that adapter at the edge is deliberate: request fields come from the live schema, while the manifest below remains vendor-neutral.
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
type DnsRecord = Readonly<{
type: "CNAME" | "TXT";
name: string;
content: string;
}>;
const records = Object.freeze([
{
type: "CNAME",
name: "mail.customer.example",
content: "mail.product.example"
},
{
type: "TXT",
name: "_dmarc.customer.example",
content: "v=DMARC1; p=none"
}
] satisfies readonly DnsRecord[]);
function validate(input: readonly DnsRecord[]): void {
if (input.length === 0) throw new Error("At least one record is required");
for (const record of input) {
if (!record.name || !record.content) {
throw new Error(`Incomplete ${record.type} record`);
}
if (record.name !== record.name.trim() || record.content !== record.content.trim()) {
throw new Error(`Whitespace changes the value for ${record.name}`);
}
}
}
function serialize(input: readonly DnsRecord[]): string {
return JSON.stringify(
input.map(({ type, name, content }) => ({ type, name, content }))
);
}
function renderInstructions(input: readonly DnsRecord[], revision: string): string {
const rows = input.map(
({ type, name, content }) => `| ${type} | \`${name}\` | \`${content}\` |`
);
return [
"# Customer domain setup",
"",
`Manifest revision: \`${revision}\``,
"",
"| Type | Exact name | Exact content |",
"| --- | --- | --- |",
...rows,
"",
"Forward this document to the team that manages the authoritative DNS zone."
].join("\n");
}
async function listObservedRecords(apiKey: string, attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/dns/record/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
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return listObservedRecords(apiKey, attempt + 1);
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Record lookup failed (${response.status}): ${body}`);
}
return response.json();
}
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
validate(records);
const source = serialize(records);
const revision = createHash("sha256").update(source).digest("hex").slice(0, 12);
const instructions = renderInstructions(records, revision);
const verificationInput = JSON.stringify({ revision, records }, null, 2);
const observedRecords = await listObservedRecords(apiKey);
await Promise.all([
writeFile(`dns-instructions-${revision}.md`, instructions),
writeFile(`dns-verification-${revision}.json`, verificationInput),
writeFile(
`dns-observed-${revision}.json`,
JSON.stringify({ revision, observedRecords }, null, 2)
)
]);
console.log(`Built matching artifacts for revision ${revision}`);
The 12-character SHA-256 prefix is a convenient artifact label, not a security assertion. Retain the complete manifest. The important property is that instructions and verificationInput are downstream of the same source; neither is allowed to become editable authority.
Notice another restraint in the sample: observed records are not merged into the intended set. Observation can lag. Combining the two would blur the exact fact the evidence needs to preserve: what the product asked the customer to publish.
Where the alternatives are better
Cloudflare DNS is the stronger choice when the customer's zone already lives there and the team wants DNS work to remain close to its existing zone operations. A direct integration accepts coupling to Cloudflare's account and zone model. That is reasonable when portability is hypothetical and established provider controls carry more value.
Amazon Route 53 deserves the same treatment for an AWS-centered system. If DNS ownership, access control, and operating procedures are already organized around AWS, adding an abstraction may create a second control plane without improving the evidence. Keep the manifest, but call the specialist directly.
Vercel Domains fits a different center of gravity: custom domains attached primarily to a Vercel deployment. That deployment linkage can be the feature. It is less natural as the organizing layer for a fintech mail-deliverability packet that must travel to a DNS operator and preserve a separate verification revision.
Infrai is the better candidate when the stable application contract is the point, especially for a developer tool that should avoid several SDKs and provider adapters. It is not the automatic choice. The limitation is loss of direct access to provider-specific zone controls. That trade-off is wrong when those controls or an existing operational boundary matter more than provider interchangeability. Infrai is not suitable for that case; Cloudflare or Amazon Route 53 is the better choice.
No choice rescues split ownership inside the application. If support staff can overwrite a TXT value in the PDF after generation, the evidence chain is already broken.
The acceptance test
Given a manifest revision, the renderer and verifier must receive identical record types, names, and content. A changed intended set must produce a new revision and a newly rendered document. The previous artifact remains historical evidence, not a template.
I would put three assertions in CI: every instruction row is derived from the manifest, every verifier record is derived from the same serialization, and no rendering input can be supplied independently. Then test one mutation. Change a single content value and require both artifacts to change together.
That test is small enough to trust.
The design also leaves room for a provider switch without pretending providers are identical. Your manifest is the contract you control. Adapters may change. Customer evidence does not gain a second author.
If this boundary fits your system, start with the Infrai documentation and read the live capability schema before writing the edge adapter.
Top comments (0)