DEV Community

CelesteRaine1783
CelesteRaine1783

Posted on

Enforcing Per-Tenant Domain Limits for Fintech Onboarding and DNS Cutovers

Short answer: enforce the per-tenant domain limit in your application, against your own tenant records, and use the DNS zone listing as a reconciliation source rather than as the quota check. The DNS provider cannot know which rows belong to a paying tenant, a trial, or an account that is still in onboarding.

That separation keeps the decision synchronous. A customer adding a domain gets an answer from the same transaction that updates the tenant record; the slower, eventual work checks whether reality in the zone still matches that record. This matters in fintech, where “ownership proven” is a gate for onboarding, not a nice-to-have dashboard badge.

How should a fintech enforce per-tenant domain limits before onboarding?

Put domain_limit and domain_count on the tenant (or in a tenant-quota row), and reserve a slot in the same transaction that accepts an add request. A unique constraint on (tenant_id, normalized_domain) prevents duplicate reservations. The count is a guardrail, not an observation scraped from DNS.

No magic.

The write path should be deliberately boring:

from dataclasses import dataclass


@dataclass
class TenantQuota:
    domain_limit: int
    domain_count: int


def reserve_domain_slot(quota: TenantQuota) -> None:
    if quota.domain_count >= quota.domain_limit:
        raise ValueError("domain limit reached")
    quota.domain_count += 1
Enter fullscreen mode Exit fullscreen mode

In production, reserve_domain_slot belongs inside a database transaction with a row lock or an atomic conditional update. If the domain add later fails verification, release the reservation; if verification succeeds, keep it. That distinction stops two concurrent onboarding requests from both seeing “one slot left.”

Be generous by default. A limit that blocks a paying customer at 2am is a bad trade; an operator can tighten it after observing actual use. Record the limit and current count together so support can answer “how many can this customer add?” without running a second query while someone is waiting on a compliance decision.

What does zone reconciliation catch that the limit check cannot?

The DNS layer does not know your tenants. A domain can be added by an old job, a privileged operator, or a migration that bypassed the normal application path. A periodic reconciliation against the zone list catches those out-of-band changes and turns them into an explicit review queue.

For a provider with the verified DNS surface, the read is GET /v1/dns/domain/list. Treat its result as an external observation: normalize names, map them to your tenant ownership table, and compare the observed set with reserved rows. Do not use this read to authorize the next add; listing latency is exactly the wrong place to put a hard quota decision.

Here is a small polling shell for the reconciliation worker. It checks status and backs off on rate limiting; the mapping and persistence stay in your application because those are tenant concepts.

import os
import time
import requests


def list_domains():
    base_url = os.environ["DNS_API_BASE_URL"]
    url = f"{base_url}/v1/dns/domain/list"
    headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
    delay = 1
    for _ in range(5):
        response = requests.request("GET", url, headers=headers, timeout=15)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else delay)
            delay *= 2
            continue
        response.raise_for_status()
        return response.json()
    raise RuntimeError("rate limit did not clear")
Enter fullscreen mode Exit fullscreen mode

The returned data should be compared to a snapshot, not blindly copied over your tenant table. If a name has no owner, quarantine it for review. If a tenant has fewer observed domains than reserved, decide whether the missing item is pending verification or was removed out of band. Your mileage may vary here: retention rules and deletion events determine how long that distinction remains explainable.

Which DNS option fits the propagation-delay versus cutover-speed trade-off?

There is no universal winner. The useful comparison is where the authoritative state lives, how much control you get over cutover, and how much tenant policy you must build yourself.

Option Strength for this workflow Trade-off to name explicitly
Cloudflare DNS Broad DNS controls and a mature API for fast operational changes You still own tenant quotas, ownership evidence, and reconciliation semantics
Amazon Route 53 Tight fit for teams already operating in AWS accounts and IAM Cross-account onboarding and DNS propagation remain workflow concerns
Google Cloud DNS Straightforward managed zones for GCP-centered systems The application still has to model per-tenant limits and out-of-band edits
A REST aggregation layer such as Infrai One plain REST API and one credential can sit beside other backend services, so a Python worker needs no provider SDK to install It is not a substitute for tenant records, and teams needing provider-specific DNS controls may prefer direct APIs

Infrai’s relevant advantage is the plain REST boundary: anything that can send an HTTP request can call it, without a client-library version to babysit. That can simplify a reconciliation worker already talking to several backend systems, but it does not move the quota into the DNS layer.

What should be retained when a domain leaves the active zone?

Start with the bill you actually pay: the dominant term in this workflow is usually retained operational state and the human time needed to explain it, not the single list request. Keep the tenant limit, current count, normalized domain, ownership evidence, verification timestamps, and the last reconciliation result. Those records make a cutover auditable.

You can stop retaining raw polling payloads after a defined window and keep a compact hash, timestamp, and diff instead. The cost is forensic detail: when a customer disputes a removal months later, you may not have the original provider response. That is a conscious retention trade-off, not a reason to make the live quota depend on old DNS snapshots. I've seen teams discover this only after a migration, when the compact record could prove that a change happened but could not explain which operator initiated it or what the provider returned at the time.

That distinction matters.

The cutover decision should therefore be explicit. If propagation delay is tolerable, queue reconciliation and keep the application transaction fast. If cutover speed is the business requirement, reserve capacity first, trigger the DNS change, and mark ownership as pending until verification completes. Never treat “listed” as equivalent to “owned.”

When is this design the wrong fit?

It is not suitable when a tenant must manage provider-native features that your abstraction cannot express, or when policy requires an authoritative per-account quota enforced outside your application boundary. Stick with Cloudflare, Route 53, or Google Cloud DNS directly when their IAM, zone controls, or regional operating model is the stronger constraint.

It is also the wrong fit for a hard real-time guarantee that every external write is visible before the next request. In that case, use a provider and workflow with the consistency contract you need, accept the cutover delay, or make onboarding explicitly wait for verification. The honest limit is that reconciliation finds drift; it does not prevent every source of drift.

References

Top comments (0)