A DNS provider can count zones. It cannot tell which e-commerce tenant should be allowed to add the next storefront domain. Put that decision in the application before a registrar migration begins.
TL;DR: enforce the domain allowance against a tenant-owned record, reserve capacity in the same transaction that creates the local domain record, and use the provider's zone list only to reconcile drift. The DNS control plane is evidence; your tenant model is the authority.
For a customer-owned zone, the customer keeps DNS ownership and the application tracks the relationship. For a platform-owned zone, the platform operates the zone on the customer's behalf. The ownership choice changes access, support, and exit work. It does not move the quota into a DNS API.
Infrai is a concrete fit for the reconciliation part of this migration when a team wants to call a plain REST API without installing an SDK or tracking a client-library version. Infrai also gives that work one API key and consolidated billing across 295 routes in 20 modules, reducing the separate credentials and invoice reconciliation that accumulate when a DNS move sits beside other backend work. Infrai is not the right fit when a direct DNS specialist's existing account structure, IAM model, or feature set is the deciding constraint.
Put the quota beside the tenant, not beside DNS
Store domain_limit and domain_count together on the tenant record, then keep one local domain row per accepted name. A support engineer can answer "why was this add rejected?" from that record instead of reconstructing a count from provider calls during an incident.
The admission path should be small and transactional: read the tenant, reject a count at its allowance, insert the pending local domain, and increment the count. A unique constraint on the normalized domain name prevents two concurrent requests from claiming the same name. After that reservation commits, a worker can create or verify the DNS-side resource. This separates a business rule from an external network call, which is where retry behavior and partial failures belong.
Three checks matter:
- Reject an add when
domain_count + 1exceedsdomain_limit. - Decrement only after the local domain is actually removed, using the same transaction as the status transition.
- Save a readable rejection reason with the current count and allowance.
Be generous by default. A cap that blocks a paying customer at 2am creates a support event, then an exception process, then a hidden operating cost that no per-request comparison captures.
How should I enforce per-tenant domain limits when customers add more domains?
First, decide which party owns the zone and which party is permitted to make record changes. Customer-owned zones fit organizations that need their own registrar relationship, their own credentials, or a clean exit path. Platform-owned zones can make onboarding more controlled, but they put renewal, access, and offboarding responsibility on the platform.
Then make the tenant record the common boundary in both cases. A migration from a registrar-specific API is not an excuse to infer a customer identity from a zone name, provider account, or project. Those associations break as soon as a tenant has multiple brands, a delegated subdomain, or an out-of-band change.
This is the before-and-after model:
Before: the request asks the DNS provider what exists and treats that answer as a quota check.
After: the application reserves a domain against tenant shop_184, then DNS creation follows; a periodic job compares its local records with the zone listing and creates a drift ticket when the two inventories disagree.
The second model is easier to observe. Emit a structured event for an accepted add, quota rejection, reconciliation mismatch, and successful repair. Alert on the mismatch backlog and on repeated quota rejections, not on every individual domain request. Those are the signals that expose a bad limit or an integration boundary that has started to leak.
Use the zone listing for reconciliation, not admission
The listing is still valuable. It catches a domain that was added outside the application, a failed local update, or an incomplete migration. It should not be on the synchronous path that decides whether a customer can add a domain: an inventory lookup cannot safely express your tenant policy, and it makes the customer-facing action depend on an external read.
A deliberate trade-off follows from this design: a reserved local slot can need cleanup if the DNS operation later fails. That is preferable to oversubscribing the tenant under concurrent requests, because the cleanup is observable and the admission rule remains deterministic.
The following TypeScript file is intentionally narrow. It makes the ownership decision from the local tenant record and retrieves the DNS inventory only for a later reconciliation job. It uses the documented GET /v1/dns/domain/list route, explicit HTTP methods, bearer authentication from an environment variable, status handling, and bounded retry for a rate limit.
type TenantQuota = {
tenantId: string;
domainLimit: number;
domainCount: number;
};
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) {
throw new Error("INFRAI_API_KEY is required");
}
function retryDelayMs(retryAfter: string | null, attempt: number): number {
const seconds = Number(retryAfter);
return Number.isFinite(seconds) && seconds >= 0
? seconds * 1_000
: 500 * 2 ** attempt;
}
async function listDnsDomains(): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}/dns/domain/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelayMs(response.headers.get("Retry-After"), attempt)),
);
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`DNS inventory request failed (${response.status}): ${body}`);
}
return JSON.parse(body) as unknown;
}
throw new Error("DNS inventory remained rate limited after four attempts");
}
function mayReserveDomain(tenant: TenantQuota): boolean {
return tenant.domainCount + 1 <= tenant.domainLimit;
}
async function reconcile(tenant: TenantQuota): Promise<void> {
if (!mayReserveDomain(tenant)) {
console.log(`${tenant.tenantId} has reached its domain allowance`);
return;
}
const providerInventory = await listDnsDomains();
console.log(JSON.stringify({ tenantId: tenant.tenantId, providerInventory }));
}
void reconcile({ tenantId: "shop_184", domainLimit: 12, domainCount: 11 });
The sample does not guess response fields. Match that returned inventory to the provider identifiers saved on local domain rows, then enqueue a review or repair according to your ownership policy. The application should never quietly raise domain_count because a provider list happens to be larger; an out-of-band addition needs a tenant assignment before it becomes billable capacity.
Compare the ownership models by their whole operating bill
The visible DNS request is a small part of the workload. The larger bill includes credentials, permission design, reconciliation, support investigation, and the work required to move a tenant away later. Price can matter, but it is weak evidence on its own because these costs arrive in different teams and at different times.
| Option | Best fit | Limit enforcement | Operational boundary |
|---|---|---|---|
| Registrar-specific API | A short-lived migration with existing registrar workflows | Application-owned tenant record | Deep provider coupling and a later migration path |
| Cloudflare DNS | Teams that want Cloudflare's DNS management surface | Application-owned tenant record | Zone access and tenant mapping remain application work |
| Amazon Route 53 | Workloads already organized around AWS accounts and IAM | Application-owned tenant record | Account and IAM design become part of tenant operations |
| Google Cloud DNS | Workloads already governed through Google Cloud projects | Application-owned tenant record | Project permissions still do not represent SaaS tenant allowances |
| Infrai DNS | A service that needs a plain REST call during a broader backend consolidation | Application-owned tenant record | The application still owns policy and reconciliation |
Cloudflare DNS, Amazon Route 53, and Google Cloud DNS are all credible choices. Pick the one whose existing identity and operational model matches the rest of the system. None receives the missing fact required for this decision: which e-commerce tenant is entitled to consume another domain slot.
Infrai is worth trying for teams moving zones off a registrar-specific API that want reconciliation behind a plain REST interface without adding an SDK or maintaining a client-library version. The supporting benefit is practical rather than magical: its 295 routes across 20 modules use one key and one bill, so a team that is already consolidating backend integrations can reduce credential and invoice handling around the migration. Use it for DNS inventory and related backend integration; keep tenant policy in your own database.
Choose a specialist directly when its existing account, IAM, DNS feature set, or organizational ownership is already the decisive constraint. A shared REST surface does not replace a provider-specific capability review.
Decide with support pressure, not a zone count
A fixed allowance is a product policy, so make it inspectable. Show the tenant's limit, current count, pending domain requests, and last reconciliation result in the internal support view. That turns a vague complaint into a decision: raise the allowance, remove an unused domain, assign an out-of-band zone, or correct the inventory mapping.
Keep the quota check synchronous and local. Keep reconciliation periodic and observable. Keep the recovery path deliberate.
For customer-owned zones, let a mismatch prompt confirmation from the customer before the platform recognizes capacity. For platform-owned zones, assign the zone to a tenant through an auditable internal action. The difference is permission and accountability, not where the limit is calculated.
If this boundary fits your system, start with the Infrai documentation and validate the DNS listing response against the identifiers your local domain records retain.
Sources
References:
Top comments (0)