A new storefront needs an address before its merchant can start selling. Waiting for every recursive resolver to forget an old answer is outside the deployment pipeline's control. TL;DR: keep tenant creation inside the DNS zone you administer, identify that zone explicitly for each record mutation, and prepare traffic to serve the new hostname before publishing its record. The zone identifier selects the administrative container for the write; the record name says which DNS owner name should resolve. Neither identifier makes caches update instantly.
For a one-person SaaS shipping weekly, this separates work I can schedule from propagation I can only observe. I would spend engineering time on a repeatable cutover and rollback, not on a promise of immediate worldwide visibility.
Caches do not take deployment orders.
What changed the onboarding choice?
Suppose merchants receive cedar.shop.example and maple.shop.example. An authoritative zone for shop.example can contain both names. The DNS zone is the portion of the DNS namespace served as a unit of authority; a record is an entry under a name within that namespace. A tenant is an application concept, not a separate DNS zone by default. Creating a zone for every merchant adds delegation work without making a cached answer expire faster.
The distinction matters at cutover. If an old storefront address has already been cached, changing its authoritative record does not recall copies held by recursive resolvers. DNS TTL governs how long cached data may be reused. Lowering a TTL just before a migration cannot shorten the lifetime of answers already cached under the former TTL. For a new tenant name there may instead be a cached negative response from an earlier lookup; negative caching has its own TTL rules. Test the name before publishing it and you may create precisely that delay.
Consider a merchant who previews cedar.shop.example before the record exists. A recursive resolver can retain the negative response under the zone's negative caching parameters. Publishing the correct record immediately afterward changes the authoritative answer, but a second lookup through that same resolver can still report the cached absence. Diagnose this by querying the authoritative servers and the recursive resolver separately. Do not respond by repeatedly rewriting an already correct record: the write is not the missing step, and each retry risks making the control-plane audit trail harder to read. The onboarding UI should say that activation is pending until the route is actually reachable, without treating one failed recursive lookup as proof that the authoritative write failed.
That is the constraint behind the choice: preconfigure the application and certificates for the intended hostname, publish the DNS record, and treat successful authoritative publication and public resolution as separate states. A change of destination for an existing hostname gets a staged transition, with both old and new destinations able to serve compatible traffic while cached answers age out. A rollback has the same cache problem as the forward change.
Why does a record write need a zone identifier?
DNS clients ask about names and types. An administrative API must also know which managed zone is being edited. Its zone identifier is an administrative handle, not a field in the DNS wire answer and not the hostname a shopper visits. The relative name cedar under shop.example becomes cedar.shop.example; putting that relative name under another zone changes the target of the write. A fully qualified name alone does not identify which account, hosted zone, or authority boundary should accept a mutation.
Do not infer the write target by splitting a name at the last two labels. shop.example might be the managed zone, or it might be a delegated child of example. Public suffix boundaries are another reason that string slicing is not an authority check. Persist the zone handle alongside the zone apex and expected account in deployment configuration. Resolve it during setup, then verify the apex and authorization before tenant jobs can write. The particular shape of a zone ID belongs to the control plane; code should treat it as opaque.
The data model stays small: tenant ID, assigned hostname, intended DNS target, publication state, and the configured zone handle. Tenant IDs should select application routing, not authorize arbitrary DNS names. Validate that each generated hostname falls beneath the configured zone, and constrain writers to that zone. A mistaken write under a different zone can look successful in an API response while leaving the merchant's address unchanged.
The smallest useful publishing path
Here is an interface-level sketch, not a promise about any provider's API. It assumes a provisioned zone, an authorized writer, and an HTTPS ingress already able to recognize the assigned host. The exact DNS record type and target depend on that ingress; a CNAME at a non-apex tenant hostname is one possible arrangement, provided the target is appropriate and certificate coverage is ready.
type Zone = { id: string; apex: string };
type RecordDraft = { name: string; type: "CNAME"; target: string; ttl: number };
type DnsWriter = {
readZone(id: string): Promise<Zone>;
upsert(zoneId: string, record: RecordDraft): Promise<void>;
};
async function publishTenant(
dns: DnsWriter,
zoneId: string,
tenantLabel: string,
ingressName: string
): Promise<string> {
const zone = await dns.readZone(zoneId);
if (zone.apex !== "shop.example") throw new Error("Unexpected zone");
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(tenantLabel)) {
throw new Error("Invalid tenant label");
}
const name = `${tenantLabel}.${zone.apex}`;
await dns.upsert(zone.id, { name, type: "CNAME", target: ingressName, ttl: 300 });
return name;
}
The 300 seconds here is an example TTL, not a measured activation deadline. An implementation must check the DNS system's supported TTLs and record semantics. In particular, an upsert needs an explicit policy for existing records: a tenant onboarding retry may be harmless if the same name points to the same target, but overwriting a different target could take another merchant offline. Treat a collision as a conflict until ownership is established.
Run the job with a stable tenant key and record its intended result so retries do not allocate different hostnames. If the DNS write succeeds but the worker crashes before saving state, the next attempt should compare the existing record to the intended value. A timeout is ambiguous; it is not proof that publication failed. None of this requires exposing a control-plane ID to the storefront browser.
Check before retrying.
What changes once onboarding becomes routine?
First, distinguish control-plane health from DNS visibility. Log the tenant ID, zone handle, record name, operation outcome, and a correlation ID, while keeping credentials out of logs. Check the authoritative servers for the new answer, then sample recursive resolution separately. A single public resolver is not a global propagation oracle. Also verify the actual HTTPS host route: a DNS answer pointing to the intended ingress does not prove the application serves the right tenant.
For a larger tenant count, put publishing behind a queue with bounded retries and collision handling. Make state transitions explicit: reserved, ingress-ready, published, observed, active. The transition to active should follow the product's readiness rule, not a fixed sleep. Monitor prolonged gaps between published and observed and alert on unexpected authoritative answers. Keep old ingress routing available during destination changes until the prior TTL window and operational checks justify retirement.
The trade-off is capacity versus control. A shared zone and templated tenant names keep onboarding quick, but a writer credential for that zone has meaningful reach. Limit its scope where the control plane permits, validate names in code, and review the audit trail. Giving every tenant delegated DNS authority might fit a different product with tenant-managed infrastructure; it also changes onboarding and failure ownership. For storefronts assigned automatically, I would start with a managed parent zone and invest the saved maintenance hours in the checkout path. Revenue per hour is a useful constraint; it is not a substitute for testing the cutover.
Top comments (0)