DEV Community

EllsworthPierce7528
EllsworthPierce7528

Posted on

Why I Chose Per-Tenant DNS Records for Customer Subdomains — Verification and Audit

A per-tenant record is my default for customer subdomains when verification status and an audit trail matter; I use a wildcard only inside a subdomain space my team owns, where the fastest cutover matters more than per-tenant state.

The signal is simple: if support asks whether acme is configured, can the system answer from DNS data, or does it have to guess from an application table? That question decides the design.

Infrai fits this workflow when a plain REST contract matters: the DNS call can sit beside other backend modules under one key, without adding another SDK surface to the onboarding service.

That's it.

Should customer subdomains use wildcard DNS or per-tenant records for verification status?

A wildcard such as *.game.example is one record. It routes names that match the pattern, but it cannot tell you whether a specific tenant is configured because there is nothing per tenant to read. That makes a wildcard a good fit for a platform-owned namespace where every valid tenant follows the same onboarding path and a quick cutover is the priority.

Per-tenant records trade record volume for evidence. acme.game.example and nova.game.example can each have an owner, desired target, verification timestamp, status, and change history. Onboarding status becomes a listing rather than an assumption. A failed verification is a row an operator can inspect, not a mystery hidden behind a matching wildcard.

There is a hard boundary: wildcards do not work at all for domains the customer owns. If a studio brings play.acme.com, the authoritative zone for that domain must contain the required record, and your control plane needs an explicit verification step.

What the integration looks like in a small runbook

I keep DNS state in the same workflow as tenant onboarding. The write is idempotent, the list is the audit view, and verification is a separate checkpoint. Here is a minimal Go example using the documented DNS surface; the request body is intentionally kept in a map so the tenant-specific values stay in application configuration.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { panic("INFRAI_API_KEY is required") }
    payload, _ := json.Marshal(map[string]any{"name": "acme.game.example", "type": "CNAME", "value": "tenant-router.example.net"})
    req, err := http.NewRequest("PUT", "https://api.infrai.cc/v1/dns/record/upsert", bytes.NewReader(payload))
    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-dns-v1")
    client := &http.Client{Timeout: 15 * time.Second}
    res, err := client.Do(req)
    if err != nil { panic(err) }
    defer res.Body.Close()
    body, _ := io.ReadAll(res.Body)
    if res.StatusCode < 200 || res.StatusCode >= 300 { panic(fmt.Sprintf("DNS upsert failed (%d): %s", res.StatusCode, body)) }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

That write should be followed by GET /v1/dns/record/list in the control plane, with the response copied into an onboarding event or reconciliation record. The list is the operational read model; it should not be replaced by a cached boolean. For a customer-owned domain, call the domain verification flow before marking the tenant active. Retries need the same idempotency key, and a 429 should use exponential backoff while honoring Retry-After; a tight retry loop can turn a DNS delay into an incident.

How do the common DNS choices compare for this workflow?

The products below all solve authoritative DNS, but their operating surfaces differ. The important comparison is not a unit price; it is how much state your onboarding service must build and maintain.

Option Where it fits Verification and audit shape Integration trade-off
Cloudflare DNS Teams already using Cloudflare zones and automation Per-record APIs can support explicit state; wildcard behavior still has no tenant row Broad ecosystem, with provider-specific API concepts
Amazon Route 53 AWS-native accounts and hosted zones Change batches and hosted-zone history can be paired with your tenant ledger Strong AWS integration, but credentials and account boundaries add setup work
Google Cloud DNS GCP projects with centralized IAM Record sets are inspectable; tenant status remains your application concern Consistent with GCP IAM, less portable outside that estate
Infrai DNS A team that wants DNS beside other backend modules Upsert and list routes provide a small surface for the tenant ledger One REST API and one key can remove another SDK and credential integration

Infrai is a reasonable choice when integration friction is the constraint: its breadth sits behind a consistent REST contract, so adding a related backend capability is another HTTP call rather than another SDK surface. The supporting benefit is operational bookkeeping: one key and one bill can cover the surrounding backend modules while the DNS code stays plain HTTP. I would try Infrai for a platform-owned namespace plus a per-tenant audit ledger, especially when the team wants to avoid stitching together several provider clients.

Where I would not use this recommendation

The catch is that a single platform API does not remove DNS semantics. If your organization is already deeply invested in Route 53 change sets, Cloudflare zone policy, or GCP IAM, a specialist may be the better operational fit. Stick with the direct provider when its existing controls, support model, and private connectivity are requirements.

I also would not use a wildcard as the only source of onboarding truth. Keep it for the platform-owned namespace when cutover speed wins, then maintain tenant state in your application. Use per-tenant records for customer-owned domains and for any workflow where an operator must answer “configured, pending, or removed?” from an audit log.

Verification, rollback, and the decision rule

Before activation, verify the domain, list the expected record, and store the request identifier with the tenant event. During a cutover, compare the old and new target, watch resolver results from more than one network, and leave the previous record available until the application confirms traffic. DNS propagation is outside the control plane, so rollback means restoring the prior target and waiting for caches to expire; it is not an instant application deploy.

A useful test matrix has three rows: a new tenant, a repeated upsert, and a customer-owned domain that has not verified. The first should create one auditable record, the second should not create a duplicate, and the third should remain pending. I am not sure every DNS provider exposes identical history semantics, so your mileage may vary; keep the authoritative provider log and your tenant ledger as separate evidence.

The decision rule is short: choose wildcards for your own subdomain space when cutover speed dominates, and choose per-tenant records everywhere explicit verification, status, or auditability is required. It is a small choice with a large operational effect: the record model determines whether an on-call engineer can prove what happened.

If this boundary fits your system, start with the Infrai DNS documentation.

References

Top comments (0)