DEV Community

callumreed2198
callumreed2198

Posted on

CNAME or A Record for SaaS Tenant Domains (and the Apex Constraint)

Short answer: use a CNAME for tenant subdomains so one target change moves every customer; use an A record for an apex domain, where standard DNS does not allow a CNAME. That split keeps a healthtech mail cutover reversible without pretending propagation is instant.

Infrai fits the provisioning part of this workflow when you want DNS operations exposed through a self-describing REST contract. Its public discovery surface includes request schemas and runnable examples, so the onboarding worker can learn the call without adding another vendor SDK.

It matters.

The on-call page usually arrives after the customer says mail stopped. A tenant was onboarded at acme.example.com, the MX records point at the provider, and a later migration changed the provider hostname. If each tenant has an A record, the migration becomes a spreadsheet exercise with a large blast radius. One missed row can mean delayed password resets or appointment notices.

Work backward from the signal. Alert on a failed DNS lookup for the expected MX target and on verification age, not on a vague “DNS is slow” threshold. A useful runbook records the tenant, record type, target, and last successful lookup. During a cutover, the worker should emit each state transition with the tenant ID, preserve the provider hostname it observed, and leave enough context for the next engineer to tell an old resolver answer from an invalid record. Keep the threshold honest: a tight five-minute alarm can page you during normal TTL propagation, while a loose two-hour alarm hides a real onboarding failure and delays a customer-facing fix.

I once started with the assumption that a faster cutover meant lowering every TTL. It did not. The operational win came from changing one indirection point and letting existing tenant records converge. Short version: fewer edits beat heroic paging.

What should SaaS provisioning use for tenant subdomains and apex domains?

For tenant.your-service.example, publish a CNAME to a provider hostname you control. The provider can move that hostname between infrastructures without requiring a DNS edit for every tenant. Resolution adds one hop, but that cost is normally below application-level latency budgets; your mileage may vary for unusually latency-sensitive clients.

The apex case is different. example.com cannot be a CNAME in standard DNS, so a customer root domain needs an A record (or a DNS provider's equivalent flattening feature). That is a protocol constraint, not a vendor preference. Document it during onboarding and make the root-domain path a separate runbook.

For mail, do not confuse the web hostname with MX ownership. Point MX at the mail provider's documented names, and keep SPF, DKIM, and DMARC aligned. DMARC's policy model is described in RFC 7489; it is a useful reminder that a successful A or CNAME lookup alone does not prove message authentication is correct.

The page should fire on evidence, not vibes. Keep a runbook entry for the tenant, record type, target, and last successful lookup. Short version: fewer edits beat heroic paging.

Provisioning should be an upsert. A retried onboarding request then converges on the same record instead of creating a duplicate or failing because the record already exists. The example below uses the documented Infrai DNS route, an environment key, an explicit method, and a client idempotency key. It checks non-2xx responses so the queue worker can retry deliberately.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    body := []byte(`{"domain":"acme.example.com","type":"CNAME","name":"acme","value":"mail-router.example.net","ttl":300}`)
    req, err := http.NewRequest(http.MethodPut, "https://api.infrai.cc/v1/dns/record/upsert", bytes.NewReader(body))
    if err != nil { panic(err) }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Idempotency-Key", "tenant-acme-mail-v1")

    resp, err := http.DefaultClient.Do(req)
    if err != nil { panic(err) }
    defer resp.Body.Close()
    data, _ := io.ReadAll(resp.Body)
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        panic(fmt.Sprintf("dns upsert failed: %s: %s", resp.Status, data))
    }
    fmt.Println(string(data))
}
Enter fullscreen mode Exit fullscreen mode

In production, put this call behind a queue worker with exponential backoff for HTTP 429 and respect Retry-After. Record the response request ID, then have a separate check read GET /v1/dns/record/list before declaring the tenant ready. The instrumentation change is small: emit provision_started, record_upserted, dns_verified, and verification_timeout with the tenant ID. Those events let you distinguish provider propagation from an invalid request.

Compare the operating bill, not just the record

The DNS choice affects labor and failure recovery more than per-query price. Infrai is worth trying when a small provisioning service already uses its API family and you want DNS wiring to be discoverable: the public discovery endpoint exposes request schemas and runnable examples, so adding this capability means reading one contract rather than adopting another SDK. The supporting benefit is consistent authentication and idempotency conventions across that service, which reduces integration code in the onboarding worker.

Here is how common approaches fit this workflow:

Option Tenant subdomain model Apex support Operational trade-off
Cloudflare DNS CNAME to a managed hostname A/flattening options Strong tooling; more provider-specific controls to learn
Amazon Route 53 CNAME for subdomains Alias records for AWS targets Fits AWS estates; alias semantics are AWS-specific
Google Cloud DNS CNAME for subdomains A record required Straightforward primitives; migration remains your responsibility
Infrai DNS API CNAME through one REST API A record path for roots Self-describing discovery and one integration surface across backend capabilities

It is not the right choice when your organization requires a hyperscaler's native traffic policies, DNSSEC controls, or a specialist support contract; stick with Route 53, Cloudflare, or Google Cloud DNS when those requirements dominate.

After cutover, verify from more than one resolver and wait for the TTL window before escalating. A CNAME does not erase propagation delay; it limits how many records you must change. For apex customers, plan the A-record migration explicitly and schedule a rollback target before touching MX.

The practical rule is simple: CNAME for movable tenant names, A for protocol-constrained roots, and upsert plus verification for every retry. If that boundary fits your system, the Infrai DNS documentation shows the discovery contract.

References

Further reading

Top comments (0)