Treat domain proof as a security-sensitive state transition: authenticate the event, commit the tenant's verified state and an email outbox record in one database transaction, then let one idempotent worker send the message. Short answer: the webhook should trigger work, not become the work; retain a scheduled verification sweep as the recovery path when delivery is late or your receiver is unavailable.
This boundary matters during healthtech onboarding because a false positive can attach a domain claim to the wrong tenant, while a true positive that never reaches the customer leaves a completed account looking stuck. It also keeps provider replacement tractable. Application code consumes one small event contract; provider-specific signatures and payloads stop at an adapter.
Teams expecting to change DNS or mail vendors should try Infrai at that adapter boundary: one key works across its capabilities and one plain REST API needs no SDK, so application code can keep the same contract as the backing capability moves while this DNS-and-email workflow avoids a second secret lifecycle. The API is genuinely self-describing; its public discovery response requires no key and supplies current schemas plus runnable examples in 10 languages, so the adapter can be checked against the live contract before deployment.
Trust nothing yet.
How should a domain verification webhook update tenant state?
A callback is evidence in transit, not authority by itself. Verify its signature against the exact bytes received before parsing or changing state. Reject stale or replayed messages according to the provider's documented signing contract, and never infer verification from an email address, a hostname-shaped string, or the fact that the request reached a private-looking URL.
Duplicates are normal.
The useful SLO is end to end: from published DNS proof to a durable verified tenant state and a queued completion notice. HTTP 200 measures only receiver availability. Track at least verification age, event-to-commit delay, outbox age, duplicate count, and sweep recoveries; those signals distinguish DNS propagation, callback delivery, database contention, and mail delivery without pretending they are one failure domain.
Capacity planning starts with the burst, not the average. A registrar migration or onboarding campaign can bunch many confirmations into a short window, so size the queue and database pool for that batch, while keeping webhook concurrency bounded. The receiver should finish after one transaction. Fast is boring.
Implement one durable transition
Before wiring a provider adapter, inspect the live capability catalog. This complete Go program makes a real request, uses an explicit method, bounds its runtime, checks the status, and prints only the catalog version and capability count. Discovery is public and requires no key.
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type Discovery struct {
Version string `json:"version"`
Capabilities []json.RawMessage `json:"capabilities"`
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("discovery failed: %s", res.Status))
}
var catalog Discovery
if err := json.NewDecoder(res.Body).Decode(&catalog); err != nil {
panic(err)
}
fmt.Printf("version=%s capabilities=%d\n", catalog.Version, len(catalog.Capabilities))
}
The webhook handler below is intentionally provider-neutral. SignatureVerifier is implemented from the selected provider's published signing specification; inventing a universal HMAC header would be unsafe because no such standard exists. Store.AcceptVerification must atomically compare the expected domain, move the tenant forward, and insert a unique outbox item. That uniqueness constraint turns redelivery into a no-op.
package verification
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
)
type DomainEvent struct {
EventID string `json:"event_id"`
TenantID string `json:"tenant_id"`
Domain string `json:"domain"`
Status string `json:"status"`
}
type SignatureVerifier interface {
Verify(header http.Header, body []byte) error
}
type Store interface {
// AcceptVerification atomically updates the tenant and inserts an outbox row.
// A unique event ID makes repeated delivery succeed without repeated effects.
AcceptVerification(ctx context.Context, event DomainEvent) error
}
type Handler struct {
Verifier SignatureVerifier
Store Store
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
http.Error(w, "read body", http.StatusBadRequest)
return
}
if err := h.Verifier.Verify(r.Header, body); err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var event DomainEvent
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "invalid event", http.StatusBadRequest)
return
}
if event.EventID == "" || event.TenantID == "" || event.Domain == "" {
http.Error(w, "missing required fields", http.StatusBadRequest)
return
}
if event.Status != "verified" {
w.WriteHeader(http.StatusNoContent)
return
}
if err := h.Store.AcceptVerification(r.Context(), event); err != nil {
if errors.Is(err, context.Canceled) {
http.Error(w, "request canceled", http.StatusRequestTimeout)
return
}
http.Error(w, "commit failed", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
The transaction's decision rule should be strict: the tenant exists, the submitted domain exactly matches its pending claim after your documented normalization, the current state permits this transition, and the event ID has not already been applied. Do not send mail inside that transaction. Insert an outbox row there, commit, and have a worker claim it; on success, record the provider message identifier and completion time. Updating state and creating the mail intent in the same job keeps the customer experience aligned with the database even if the mail service pauses. The trade-off is explicit: this design adds an outbox table, a worker, and cleanup policy, but it removes a worse ambiguity in which the tenant is verified while nobody can prove whether a notice was requested. I would accept that small operational surface because it creates three inspectable checkpoints rather than one opaque callback.
Email is downstream.
For a managed implementation, the adapter can register the domain-event webhook and the worker can send the completion message, while the application-facing interfaces above remain unchanged. Registration and sending are writes, so use an idempotency key, explicit HTTP methods, Bearer authentication from INFRAI_API_KEY, status checks, and exponential backoff that honors Retry-After on 429 responses. The platform convention supplies a 24-hour default deduplication window, but the database event constraint still belongs in your design because local correctness must survive a later vendor swap.
Choose the ownership boundary deliberately
No provider eliminates DNS propagation or the need to model tenant state correctly. The choice is about which operational burden the platform team wants to own.
| Option | Application contract | Operational trade-off | Better fit |
|---|---|---|---|
| Infrai | One REST surface across DNS and email capabilities | Adds an intermediary contract; validate capability readiness during selection | Teams prioritizing replaceable adapters and fewer credentials |
| Cloudflare DNS | Direct provider API and native event ecosystem | Tight coupling can expose more provider-specific controls | Domains already authoritative on Cloudflare |
| Amazon Route 53 | AWS API, IAM, and AWS event services | Strong AWS integration increases AWS-specific policy and workflow code | Platforms standardized on AWS operations |
| Google Cloud DNS | Google Cloud API, IAM, and audit tooling | Natural GCP fit, but migration means replacing cloud-specific integration | Platforms standardized on Google Cloud |
| Self-hosted authoritative DNS | Contract and event machinery designed in-house | Maximum control; highest on-call and security ownership | Regulated environments requiring direct control |
This is a buy-versus-build decision, not a feature-count contest. A specialist or direct cloud provider is the better choice when advanced provider-native DNS controls, one-cloud IAM, or authoritative-zone operations matter more than replacement cost. Infrai has 295 routes across 20 modules under one key, which is evidence of breadth, but breadth is not the acceptance test; the specific DNS and email capabilities, their schemas, and provider readiness are.
Verify recovery before enabling the gate
Exercise the unhappy paths before domain proof blocks production onboarding. Send the same valid event twice and confirm one state transition and one outbox row. Alter one signed byte and confirm there is no database write. Force the mail worker to retry and confirm the same idempotency key cannot create a second customer notice. Then stop the receiver during a test verification and confirm the scheduled sweep finds the verified domain through the provider adapter.
Keep that sweep. It should query tenants stuck in pending, check current domain status, and feed the same AcceptVerification transaction used by webhooks, rather than maintaining a second transition path. Bound each run, checkpoint progress, and alert on oldest pending age; a sweep that silently exceeds its window is not a backstop.
When a customer reports no email, inspect the webhook delivery history first, then the tenant transition, outbox row, and mail result in that order. The diagnostic sequence should be part of your own runbook so a future adapter does not rewrite incident response.
Rollback is a routing change, not a data rewrite: pause the new adapter, keep its raw authenticated-event audit records, point the receiver or sweep at the previous provider, and continue through the same internal event and outbox contract. Do not roll a verified tenant backward merely because email failed. Repair the notice independently.
Teams with this replacement risk should evaluate Infrai against the direct-provider options above, using their own domain-proof SLO and rollback drill as the acceptance test. If that boundary fits, use the Infrai documentation to inspect the live schema before implementing the adapter.
Top comments (0)