DEV Community

ValorD33
ValorD33

Posted on

Custom Domain Onboarding: Show DNS Records, Copy or Write Them for Customers

Short answer: show the exact SPF, DKIM, and DMARC records first, then let a customer explicitly authorize a write; this keeps onboarding honest when the DNS zone is customer-owned and prevents your intent from drifting away from what the internet publishes.

In a property-management product, a domain step is rarely owned by the person who clicked “add domain.” It may be an office manager, a managed-service provider, or a registrar administrator. Treating DNS as a form you can silently mutate creates a bad handoff: the UI says “verified,” while a stale TXT record still sends leasing notices to spam.

I care about that mismatch because deliverability failures are often delayed. A record can look right in an internal database and still be absent from public DNS, cached at a resolver, or superseded by a second SPF record. The onboarding design has to make those states visible.

What should property teams verify before publishing mail records?

Start with an intent record in your own database. Store the domain, record type, owner name, expected value, and the key version that generated it. Store an observation separately: resolver, timestamp, observed value, and pass/fail result. Never overwrite the expected value with the observation. That distinction is your drift detector.

For example-property.com, the screen might show these rows:

Type Name Value (example) Why it exists
TXT @ v=spf1 include:mailer.example -all Authorizes a sending path
TXT s1._domainkey v=DKIM1; k=rsa; p=... Verifies a signature
TXT _dmarc v=DMARC1; p=none; rua=mailto:dmarc@example-property.com Reports alignment results

The values above are illustrative; the sender's published values must come from the system that signs and sends mail. SPF has a one-record constraint in practice: if the customer already has an SPF TXT value, append an authorized mechanism rather than creating a second v=spf1 record. DKIM names are selector-specific, so a key rotation should create a new selector, wait for propagation, then retire the old one. DMARC policy is a domain-owner decision; p=none is a useful observation phase, not proof that messages are aligned.

Make the copy action boring and precise. Give each field a copy button, preserve whitespace in the value, and show whether the DNS console expects a relative name (_dmarc) or a fully qualified name. A screenshot is not enough; registrars normalize names differently.

Check twice.

How do copy and write workflows prevent custom-domain drift?

Copy mode should be the default. The customer publishes records in the authoritative zone, and your verifier polls public DNS until the observed tuple matches the expected tuple. A write mode can exist for zones your team controls, but it needs a narrow scope: one zone, three record types, an audit entry, and a preview of the exact mutation.

Here is the state machine I use in backend code. It keeps a successful verification from masking a later change.

from dataclasses import dataclass
from datetime import datetime

@dataclass
class DnsIntent:
    fqdn: str
    record_type: str
    expected: str
    key_id: str

def classify(intent: DnsIntent, observed: str | None, checked_at: datetime) -> dict:
    if observed is None:
        return {"state": "missing", "checked_at": checked_at.isoformat()}
    if observed.strip() == intent.expected.strip():
        return {"state": "verified", "checked_at": checked_at.isoformat(), "key_id": intent.key_id}
    return {
        "state": "drifted",
        "checked_at": checked_at.isoformat(),
        "expected_hash": hash(intent.expected),
        "observed_hash": hash(observed),
    }
Enter fullscreen mode Exit fullscreen mode

Do not mark a domain complete on a successful write response. Re-query an independent recursive resolver, record the observation, and require the customer to retry after a propagation window. Your UI should distinguish “write accepted,” “record visible,” and “mail authentication passing.” Those are three different facts.

There is a catch: automatic writes are not suitable when a customer has split DNS, DNSSEC change controls, or a provider that requires a ticket for production edits. Stick with copy mode there. Choose write mode only when your authorization boundary is explicit and rollback means restoring the prior value, not deleting an unknown record.

Where do SPF, DKIM, and DMARC checks fail in production?

The first failure is duplicate intent. A property group may send from rentals.example-property.com while the onboarding form verifies the parent domain. Keep the exact envelope and header domains in the verification job, and test alignment, not just TXT presence.

The second is stale ownership. A tenant can pass a token check, finish onboarding, and later move the zone to another registrar. Verification should expire or recheck on a schedule; a green badge is a current observation, not a permanent entitlement.

The third is retention. Keeping every DNS response forever creates privacy and storage work without improving diagnosis. I retain the current expected record, the latest observation, and a bounded change history. That history includes the selector key, resolver used, response TTL, and the actor who approved a write; without those fields, an engineer staring at a 421 rejection cannot tell whether a sender used an old DKIM key, a resolver served stale data, or a customer edited the zone. When an incident needs older evidence, missing history has a cost: you may not be able to prove when a policy changed. That is a deliberate trade-off, and it belongs in the runbook.

DMARC reports add another edge. Aggregate reports can contain identifiers and can arrive after the customer has changed policy. Parse them asynchronously, tie them to the policy version observed at send time, and avoid treating a report as a real-time delivery receipt. RFC 7489 describes the reporting model; it does not promise inbox placement.

A practical decision rule for the onboarding team

Use copy mode when the customer owns the zone, when edits require approval, or when you cannot guarantee an authoritative API. Offer write mode for a platform-owned zone with scoped credentials, an idempotent mutation, and a visible audit trail. In both modes, keep intent and observation as separate records and expose drift as a first-class state.

I once assumed a “verified” flag was enough. Then a resolver returned the old DKIM selector while our database had already rotated the key. The fix was not a clever retry; it was storing selector versions and showing the observed answer next to the intended one. Small change. Big reduction in guesswork.

Your mileage may vary because resolver caches, registrar UX, and mail streams differ. Measure the time from intent creation to public visibility, the percentage of domains that drift after verification, and the number of support tickets caused by name normalization. Those metrics tell you whether an automated write is actually helping.

References

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

The expected-vs-observation separation is the part most integrations skip, and it's exactly where the silent drift comes from: once you overwrite the expected value with whatever the last lookup returned, "verified" stops meaning anything because you're comparing the zone against itself.

The three-states UI point also generalizes beyond domains. Write accepted, publicly visible, and actually working are three different facts in any onboarding that touches external infrastructure - webhooks, OAuth callbacks, TLS. A green badge that's a snapshot of one observation decays silently, which is why your expiry point matters more than it looks. Did you settle on a recheck schedule for the drifted state, or does the customer trigger the retry after the propagation window?