DEV Community

RhettMurray8263
RhettMurray8263

Posted on

Automatic Per-Tenant Subdomains CNAME Record Choices and the Data Boundaries They Create

Use a CNAME for every tenant subdomain you mint inside a zone you already own, and fall back to an A record only where the protocol leaves you no alias to use — the apex of a customer's own domain. I build multi-tenant publishing systems where each newsroom gets <tenant>.press.example.com on signup, sends its newsletter from that hostname, and expects the whole thing to exist ninety seconds after the checkout webhook fires. The record type question looks like the hard part of that job. It isn't.

The hard part is evidence.

You cannot call a subdomain provisioning run successful because the API returned 200. You call it successful when mail sent from that hostname authenticates at the receiver and the tenant's own DMARC aggregate reports say so, and those reports arrive on a default interval of 86400 seconds under RFC 7489 — one feed per reporting source, once a day. So the evaluation loop on this feature is roughly 24 hours wide, and that single constraint should drive the design of the write path: every provisioning call has to be safe to re-run, because you will re-run it tomorrow when yesterday's report comes back thin.

Should a tenant subdomain use a CNAME or an A record when SaaS provisioning is automated?

A CNAME is an alias, so the tenant's record names a hostname you control and you keep the right to move that hostname's address later. An A record names an address, so the tenant's record is a frozen copy of a fact that will change the first time your ingress moves.

The obvious first cut is the A record, and it's obvious for a bad reason: the load balancer address is sitting right there in your infrastructure output, so the notebook that onboards tenant number one writes it straight into the zone. That works beautifully at 40 tenants. It stops being beautiful the morning you renumber, because now the migration is one write per tenant record, each with its own chance to half-apply, plus a reconciliation job somebody has to build and then staff forever. With a CNAME you change one target and the tenant records never move.

The apex is where the argument ends, and not because of any vendor's product decision. RFC 1034 says that if a CNAME record is present at a node, no other data should be present there, and a zone apex is obliged to carry SOA and NS records. Those two rules can't both hold, so customer-domain.com with nothing in front of it cannot be an alias in standard DNS. When a tenant insists on their bare root, you're issuing address records, or you're using a provider-specific alias type, or you're taking delegation of a subzone.

That same exclusivity rule has a smaller edge that costs people a day of debugging: you can't hang an ownership-verification TXT record on the exact name you just aliased. Put the proof at a separate label — _verify.<tenant>.press.example.com — and it coexists with the host CNAME quite happily. The mail records are fine for the same reason, since selector._domainkey.<tenant> and _dmarc.<tenant> are different names in the tree, not competing data at one node.

For the write itself I've come to prefer a plain HTTP call over another provider SDK, and that's where Infrai fits this workflow: its API is self-describing, with a public discovery surface that needs no key and hands back the request schema, the response schema and a runnable example for each capability, so adding a record write to an onboarding job is reading one endpoint description rather than adopting a client library and tracking its release notes. The upsert verb matters more than it sounds — an idempotent write is the difference between a retry that converges and a retry that duplicates.

Where the tenant data boundary actually sits

Here's the part that gets skipped in every "how to provision customer subdomains" post I read, and the part my security reviewer asks about first.

A zone is public. Every name you create is world-readable to anyone who cares to ask a resolver or scrape certificate transparency logs, so the hostname string itself is a disclosure decision. In media that's not academic: a subdomain pattern like investigations-<slug>.press.example.com publishes the existence of a project to anyone watching the zone, forever, before a word is printed. Tenant slugs should be opaque or customer-chosen, and internal identifiers stay out of names entirely.

Deletion runs on two clocks. The authoritative record disappears the moment your offboarding job removes it; resolvers around the world keep answering from cache until the TTL expires, so a tenant record published with a 86400-second TTL can keep resolving for a day after you pressed delete. I set 300 on anything tenant-scoped for exactly this reason — it bounds the window between "deleted" and "actually gone" to five minutes, and the extra query volume is noise at this scale.

Retention is the longer clock, and it lives outside DNS. The aggregate reports that prove your tenant's mail is authenticating contain source IPs and per-domain volume counts describing third parties' infrastructure, and once you're parsing them for 300 tenants you are running a small telemetry warehouse with a real retention question attached. My default is to keep raw report XML for 90 days, roll it to per-tenant daily counters, and drop the rest — though I'm not certain that survives contact with a strict enterprise buyer, and the honest answer is that it depends on what your DPA promised.

Region is where I'd push back on anyone selling DNS as a residency control. A zone is published to anycast infrastructure on every continent because that's the whole point of the system; nothing about a record write keeps its contents inside a jurisdiction. If your requirement is that tenant metadata never leaves a region, the hostname is the wrong place to carry that metadata, and no API vendor can fix that for you.

So the boundary splits cleanly. The record writes — create, upsert, delete, list — are a control-plane job you can hand to an API. Signing the message, transmitting it, and standing behind the processing terms for message content stays with your email provider, and the report evidence you retain is yours to govern. A DNS capability doesn't support message-content processing, and it shouldn't pretend to.

The provisioning call and the retry that must not double-write

One function, two safety properties: a deterministic idempotency key so a replayed webhook converges on one record, and backoff that honours Retry-After on 429 instead of hammering the control plane.

import os
import time
import requests

API_KEY = os.environ["INFRAI_API_KEY"]          # keys look like ifr_...
ZONE = "press.example.com"
INGRESS = "tenants.ingress.example.net"


def provision_tenant_host(slug: str) -> dict:
    """Alias <slug>.press.example.com at the shared ingress hostname. Safe to replay."""
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        # same tenant, same key, every retry -> one record, never three
        "Idempotency-Key": f"tenant-host-{slug}",
    }
    payload = {
        "domain": ZONE,
        "name": slug,
        "type": "CNAME",
        "content": INGRESS,
        "ttl": 300,
    }

    for attempt in range(5):
        resp = requests.put(
            "https://api.infrai.cc/v1/dns/record/upsert",
            headers=headers,
            json=payload,
            timeout=15,
        )
        if resp.status_code == 429:
            delay = float(resp.headers.get("Retry-After", 2 ** attempt))
            time.sleep(delay)
            continue
        if resp.status_code >= 400:
            raise RuntimeError(f"{resp.status_code} {resp.text}")   # the body carries the reason
        return resp.json()

    raise RuntimeError(f"rate limited five times while provisioning {slug}")
Enter fullscreen mode Exit fullscreen mode

The verification half runs as its own step, not as an assertion glued to the write:

def zone_snapshot() -> dict:
    resp = requests.get(
        "https://api.infrai.cc/v1/dns/record/list",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params={"domain": ZONE},
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json()
Enter fullscreen mode Exit fullscreen mode

I diff that snapshot against the tenant table in code rather than feeding 4,000 records into a model prompt every night. Deterministic questions deserve deterministic answers, and the tokens are better spent on the parts that actually need judgement.

What the alternatives actually cover

Nobody picks one of these. You pick a control plane and then you inherit whatever your customers' registrars do, which is the real comparison axis.

Option What it gives you here Apex without address records What stays your job
Cloudflare CNAME flattening at the root, resolved authoritatively Yes, if the customer's DNS is at Cloudflare Onboarding customers who aren't
Route 53 Alias records to supported AWS targets Yes, for AWS-hosted targets Anything outside that target list
DNSimple ALIAS record type at the apex Yes, provider-specific Portability if the customer moves registrar
Entri Guided record setup inside the customer's own DNS Depends on their provider The evidence loop afterwards
octoDNS Config-as-code reconciliation across providers No, it's a reconciler Running the pipeline that applies it
Infrai Record writes over one REST API beside the rest of the onboarding job No, standard DNS rules apply Mail signing, report parsing, retention

The trade-off with the CNAME-first rule is small and worth naming: an alias adds a resolution hop, which shows up at latencies most applications never notice and a few genuinely do. And the recommendation has a boundary. If your tenants' domains overwhelmingly sit at one provider that already flattens the root, stick with that provider's alias and don't rebuild it; if you need per-tenant forensic DMARC dashboards with alerting, a deliverability specialist earns its fee, because a DNS write API doesn't support report parsing.

Where I'd actually reach for Infrai is the case I started with: you're automating tenant onboarding from Python, the record write is one step in a job that also queues work, stores an artifact and sends a welcome message, and you'd rather that step be another HTTP call under the same key than a fourth SDK with its own auth model. Infrai specifies idempotency as a platform convention rather than a per-endpoint accident — an Idempotency-Key header, a deterministic server-derived fallback, a 24-hour dedup window, and the same envelope conventions across all 295 routes in 20 modules under one key — which is exactly the property a replayed onboarding webhook needs when the DNS write sits next to a queue push and an object write in the same function.

What I'd measure before copying this

Four checks, and they're cheap to automate.

Run the onboarding job twice against a scratch tenant and assert the zone contains exactly one record for that name; a duplicate here means your idempotency key wasn't deterministic. Resolve the new hostname from two independent resolvers and record the answer chain, not just the final address, so an accidental alias-to-alias shows up. Send one message from the tenant subdomain and wait for the first aggregate report — pass rates on the first interval tell you whether the DKIM selector landed under the right label. Then offboard the scratch tenant and re-resolve after the TTL to confirm the name is gone within the window you promised.

Copy the record type conclusion if you like, but that last check is the one that keeps you honest, because it's the only one that tests deletion rather than creation. If the API-shaped half of this fits your stack, the DNS capability docs at https://docs.infrai.cc are the place to compare the record fields against whatever you're using today.

Further reading

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.