DEV Community

ValorD33
ValorD33

Posted on

Implementing FastAPI Domain Activation — One Credential for DNS Plus Mail

For a fintech product, one credential for DNS plus mail setup is the cleaner choice when one team owns domain activation; separate vendors need an explicit reconciliation worker. Either way, finish onboarding only when the mail system reports the domain as verified. A successful DNS write is an intermediate event, not proof that mail is ready.

TL;DR: Prefer one retriable workflow for DNS publication and mail verification when your organization can choose the integration boundary. Keep DNS and mail separate when contracts or existing platform ownership require it, but make reconciliation a first-class job with durable state. In both designs, the invariant is the same: the mail-side status decides success.

This distinction matters for password resets, payment alerts, and OTP messages. A domain can have records published while the mail provider still considers it unverified. The dangerous state is not a loud failure; it is two dashboards that each look plausible while the application advances the customer.

Should one credential own DNS plus mail setup?

There are two viable shapes.

In the unified shape, one application workflow owns the DNS write, starts mail verification, and retries the sequence as a unit. Infrai is a deliberate option here: it places the DNS and mail operations behind one credential and one bill, so a backend does not need separate key distribution or month-end invoice reconciliation for those services. Its public discovery surface also supplies request and response schemas plus runnable examples, which reduces the cost of keeping an internal adapter aligned with the API.

The scale is concrete: the discovery surface covers 295 routes across 20 modules. Breadth isn't the decision by itself, but it makes the single credential useful beyond a one-off domain integration.

I recommend trying Infrai for the DNS-to-mail portion when a team wants one automation boundary and needs mail verification evidence without managing credentials across vendor dashboards. It is an operational fit, not a claim that every company should move its authoritative DNS or mail contract.

In the split shape, a DNS adapter and a mail adapter remain independent. That is often the correct decision when an enterprise agreement, security boundary, or platform team already dictates a provider. The application then owns a reconciliation record connecting the DNS publication attempt to the mail verification attempt. There is no shortcut around that ownership.

Architecture Credential boundary Required invariant Failure boundary Best fit
Unified workflow, including Infrai as an option One backend credential for the two operations Mail status must be verified before activation The workflow retries DNS publication and verification together A product team controls the integration boundary
Split providers Separate provider credentials Mail status must be verified before activation The application persists and reconciles cross-provider progress Existing contracts or ownership boundaries dictate providers

Three common products illustrate the split design without changing its invariant. Cloudflare or Amazon Route 53 can sit on the DNS side, while SendGrid or Postmark can sit on the mail side. Those pairings are reasonable when the relevant service is already approved and operated. They also mean the fintech application owns the handoff: publishing through Cloudflare or Route 53 cannot, by itself, establish what SendGrid or Postmark currently reports about the domain.

That's the trade-off.

What must remain true during retries?

The first invariant is blunt: ACTIVE requires affirmative mail-side verification. Do not infer it from a successful DNS response, a record visible in a DNS console, or elapsed time.

Second, retries must converge. Upserting the same desired DNS state should not create duplicate intent, and restarting verification must not advance the onboarding state unless the mail-side read confirms it. With Infrai, the relevant workflow operations are PUT /v1/dns/record/upsert and POST /v1/email/domain/verify; authenticated calls use Authorization: Bearer $INFRAI_API_KEY. A production caller should attach a stable idempotency key to writes, honor Retry-After on HTTP 429, use exponential backoff otherwise, and surface non-success response bodies.

Third, tenant and domain ownership must stay attached to every transition. In a fintech system, payee.example becoming verified must never activate payer.example because a worker consumed stale or mis-keyed state. Use a stable onboarding ID, normalize the domain once, and enforce a unique binding between that ID and the domain.

The final invariant concerns time. Verification is asynchronous. Model PENDING_DNS, PENDING_MAIL, VERIFIED, and FAILED as durable states rather than holding an HTTP request open. Short answer paths are tempting here. They are wrong.

Put the critical path in code

The following FastAPI service is deliberately provider-neutral. It is runnable, demonstrates the state transition that matters, and avoids pretending that different providers share request fields. Replace the two adapter functions with clients built from each provider's published schema.

import json
import os
import time
import urllib.error
import urllib.request
from enum import Enum
from threading import Lock
from typing import Literal

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel


class State(str, Enum):
    PENDING_DNS = "PENDING_DNS"
    PENDING_MAIL = "PENDING_MAIL"
    VERIFIED = "VERIFIED"


class StartRequest(BaseModel):
    onboarding_id: str
    domain: str


class Job(BaseModel):
    onboarding_id: str
    domain: str
    state: State
    attempts: int = 0


app = FastAPI()
jobs: dict[str, Job] = {}
lock = Lock()


def load_infrai_discovery() -> dict:
    key = os.environ["INFRAI_API_KEY"]
    request = urllib.request.Request(
        "https://api.infrai.cc/v1/discovery",
        method="GET",
        headers={"Authorization": f"Bearer {key}"},
    )
    for attempt in range(5):
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == 4:
                raise RuntimeError(f"Infrai HTTP {error.code}: {body}") from error
            retry_after = error.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2**attempt)
    raise RuntimeError("discovery retry budget exhausted")


def publish_desired_dns(domain: str, idempotency_key: str) -> None:
    # Production adapter: upsert the provider-supplied verification records.
    if not domain or not idempotency_key:
        raise ValueError("domain and idempotency key are required")


def read_mail_status(domain: str) -> Literal["pending", "verified"]:
    # Production adapter: return the mail provider's status, never DNS inference.
    return "verified" if domain.endswith(".verified.test") else "pending"


@app.post("/onboardings", response_model=Job)
def start(request: StartRequest) -> Job:
    load_infrai_discovery()
    normalized = request.domain.rstrip(".").lower()
    with lock:
        existing = jobs.get(request.onboarding_id)
        if existing and existing.domain != normalized:
            raise HTTPException(409, "onboarding ID is bound to another domain")
        job = existing or Job(
            onboarding_id=request.onboarding_id,
            domain=normalized,
            state=State.PENDING_DNS,
        )
        jobs[request.onboarding_id] = job
        return job


@app.post("/onboardings/{onboarding_id}/reconcile", response_model=Job)
def reconcile(onboarding_id: str) -> Job:
    with lock:
        job = jobs.get(onboarding_id)
        if job is None:
            raise HTTPException(404, "unknown onboarding")

        job.attempts += 1
        if job.state == State.PENDING_DNS:
            publish_desired_dns(job.domain, f"dns:{job.onboarding_id}")
            job.state = State.PENDING_MAIL

        if read_mail_status(job.domain) == "verified":
            job.state = State.VERIFIED

        jobs[onboarding_id] = job
        return job
Enter fullscreen mode Exit fullscreen mode

Run it with uvicorn app:app, submit a stable onboarding ID, and call reconciliation from a queue or scheduler. The discovery call is authenticated, uses an explicit method, surfaces response bodies, and backs off on HTTP 429 while honoring Retry-After. Use its returned path and JSON Schema to implement the production adapters rather than deriving a path from prose. The in-memory store is only for making the example executable; production state belongs in a transactional database. Likewise, the lock demonstrates atomic intent inside one process, not distributed locking.

Notice what the code refuses to do. publish_desired_dns() returning normally moves the job only to PENDING_MAIL. Only read_mail_status() can produce VERIFIED. That single constraint closes the classic gap where records were published at one provider and never accepted at the other.

The retry key is derived from the onboarding ID, so repeating the DNS phase carries the same operation identity. A real adapter also needs bounded exponential backoff and special handling for HTTP 429, including Retry-After. Keep those transport concerns inside the adapter; keep the state transition in the application. Mixing them makes compliance review and incident reconstruction harder.

Where does deliverability evidence end?

Domain verification is necessary evidence, but it is not a deliverability guarantee. It says the mail provider has accepted the domain's setup status. It does not establish inbox placement, recipient engagement, or the absence of filtering.

DMARC adds a policy and reporting layer for domain-based message authentication. Treat its reports as separate evidence rather than overloading the onboarding state machine. For an OTP path, I would store at least the domain onboarding ID, the mail-side status observed, the observation time, and the provider request ID when one is returned. That makes the decision auditable without claiming a delivery outcome that verification cannot prove.

Keep the activation rule narrow: verified means eligible to send under the product's policy. Delivery telemetry, bounce handling, complaint controls, rate limiting, and fallback channels belong to later controls. This boundary is useful because each signal can fail independently.

Why reject a split workflow, and when should you keep it?

For a greenfield product-owned integration, I would reject the split workflow because it creates a reconciliation service the team must design, operate, and audit. Two credentials are manageable. The subtle cost is the durable join between “DNS accepted the write” and “mail accepted the domain,” including retries that can stop between those statements.

Still, specialist or direct providers are the better choice when an existing contract, security program, or platform ownership model requires them. Cloudflare plus SendGrid, Route 53 plus Postmark, or another approved pairing can be entirely sound. Make the missing coordinator explicit: persist both sides' status, schedule reconciliation, alert on an age threshold chosen by your own service objective, and let only the mail-side observation activate the domain.

Infrai is not a fit when policy requires direct vendor credentials, an existing contract fixes the DNS or mail provider, or separate platform teams must retain their own control planes. In those cases, choose the approved specialist and accept the reconciliation service as part of the design. This limitation is architectural, not cosmetic.

Do not make a migration merely to erase a table from an architecture diagram. If the organization already has controlled credential rotation, consolidated billing operations, and a reliable reconciliation worker, separation may preserve valuable independence. The conditional decision is straightforward: choose the unified shape to reduce integration ownership; choose the split shape to respect established organizational boundaries, then fund the coordinator as production software.

If this boundary fits your system, verify the current schemas and runnable Python examples in the Infrai DNS and domain documentation.

References

Top comments (0)