Reconcile each tenant table row against live DNS observations as a three-state decision: confirmed, pending, or conflicting. Do not fail fintech onboarding merely because one scheduled pass cannot see the expected proof. Require repeated agreement before confirmation, preserve the prior observation, and page only when a deadline or a security invariant is at risk.
TL;DR: compare tenant intent with two independently collected snapshots, but let the onboarding deadline decide urgency. Absence before the deadline is pending; repeated absence after it is stalled. A value assigned to another tenant is an immediate conflict. This separates propagation delay from dangerous drift and makes cutover speed a policy choice instead of a scheduler side effect.
How should a tenant table be checked against live DNS?
The tenant table is desired state. DNS is observed state. Neither describes the whole workflow: the database can retain an abandoned attempt, and a lookup can return an answer from an earlier change.
So the scheduled check should join each active onboarding attempt to the exact owner name and proof value it issued, then classify the result with its previous observation. A dashboard count is not an incident. Ask what page fired and what a responder can do about it.
| Intent | Observation | State | Action |
|---|---|---|---|
| Active, before deadline | Expected value absent | Pending | Retry; do not page |
| Active, after deadline | Absent in repeated snapshots | Stalled | Alert the onboarding owner |
| Active | Value belongs to another tenant | Conflicting | Block completion and page |
| Cancelled | Proof remains | Residual | Queue a cleanup review |
This is where noisy systems go wrong: they treat every difference equally. Ordinary waiting becomes alert volume, while a cross-tenant value can disappear into the same undifferentiated counter.
Collect evidence before making a verdict
Each run should read a bounded tenant snapshot, normalize domain names and proof values, query through the same resolver path used by verification, and persist the outcome with a timestamp and run identifier. The next run compares desired state, current evidence, and prior evidence.
Two agreeing snapshots reduce the authority granted to one transient absence. The exception is a proof that maps to another active tenant; delaying that decision increases exposure without helping propagation. This is an explicit trade-off. Put deadline_at and required_agreements in policy, rather than scattering sleep durations through worker code.
Keep lookup errors separate from negative answers. An unavailable resolver does not prove absence.
No verdict.
The scheduler may live in a Node.js service, a queue consumer, or a system timer. Keep its contract runtime-independent: immutable tenant rows go in, typed observations come out, and a policy step decides state. This Go example shows the decision boundary without binding it to a DNS product.
package reconcile
import (
"strings"
"time"
)
type Tenant struct {
ID, ExpectedProof string
Deadline time.Time
}
type Observation struct {
Values []string
Err error
}
type Result struct {
State, Reason string
}
func Decide(now time.Time, t Tenant, current, previous Observation) Result {
if current.Err != nil {
return Result{"unknown", "lookup failed"}
}
match := func(o Observation) bool {
for _, value := range o.Values {
if strings.TrimSpace(value) == t.ExpectedProof {
return true
}
}
return false
}
if match(current) && previous.Err == nil && match(previous) {
return Result{"confirmed", "proof agreed across snapshots"}
}
for _, value := range current.Values {
v := strings.TrimSpace(value)
if v != "" && v != t.ExpectedProof {
return Result{"conflicting", "unexpected proof observed"}
}
}
if now.Before(t.Deadline) {
return Result{"pending", "proof has not stabilized"}
}
return Result{"stalled", "deadline passed without agreement"}
}
The example intentionally does not turn Err into absence. Persist diagnostic details with the observation, but use bounded outcome labels for metrics. Domain names and proof values belong in access-controlled diagnostic records, not metric labels.
The collector is only half the problem. Ownership proofs can still be confused when their identity and lookup boundaries are vague.
A proof should identify the onboarding attempt, not merely the account. Retries and cancelled attempts otherwise become ambiguous. Store the exact expected value beside tenant intent, compare it as an opaque value after the normalization defined by the challenge protocol, and never silently adopt an unexpected record into the database.
Domain boundaries are another trap. DMARC shows why DNS policy discovery is more than arbitrary string matching: RFC 7489 defines records at _dmarc and describes discovery involving the organizational domain. A custom TXT ownership challenge needs its own owner name and lookup contract. It must not inherit DMARC discovery behavior merely because both mechanisms use TXT records.
In fintech onboarding, proof of control gates a business transition. Verify the challenge that onboarding issued. Mail policy may be a later readiness check, but it is not interchangeable evidence.
Verify transitions, not isolated rows
Begin in shadow mode: write observations and proposed decisions, but do not complete onboarding or page. Test sequences for present, absent, conflicting, malformed, expired, and lookup-error observations. A single fixture cannot show whether the state machine resists a transient answer.
Before enforcement, verify five invariants:
- Every active tenant appears once in the evaluated snapshot, including lookup failures.
- An error becomes
unknown, neverabsentorstalledby itself. - Confirmation requires the configured number of agreeing observations.
- A foreign proof blocks completion immediately.
- An alert names the tenant, domain, deadline, current state, previous state, and run identifier.
Compare aggregate shadow results with onboarding state, then inspect every conflict and a sample of pending records. A green percentage can hide the one row that matters. I distrust that dashboard because it cannot tell the responder whether the page is actionable; the alert must name the crossed deadline or invariant.
Track run freshness, evaluated tenants, bounded outcomes, lookup-error ratio, and time pending. Also reject overlapping runs, since two workers racing on one tenant generation can manufacture apparent state changes unrelated to DNS.
Roll back decisions, not evidence
Separate collection from enforcement. Enable snapshots first, then decisions, notifications, and finally automated completion. Each stage should be independently disabled while observations continue accumulating. If a tenant row changes between snapshot creation and commit, discard the old decision and evaluate the newer generation.
The approach has a concrete limitation: two-snapshot agreement delays a valid cutover by at least one collection interval. A shorter interval improves speed but increases resolver and worker load; accepting one observation is faster but grants transient data more authority. When that delay is unacceptable, use an explicit synchronous verification attempt for the user-facing path and keep scheduled reconciliation as the backstop.
Short-lived ambiguity is expected.
The runbook rule should remain readable at 3am: complete after the required agreements, block any cross-tenant conflict, and alert on absence only after the tenant deadline. Record the policy with the decision so the page explains why it fired.
References
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
Top comments (0)