The operational constraint decides the design: a media tenant must prove domain ownership before onboarding completes, but the DNS layer has no idea which tenant owns a row in your application. Short answer: count active domains in your own tenant table for the admission check, then reconcile that count with the provider's zone list on a schedule. A live DNS count can observe zones; it cannot enforce your customer quota.
I keep the admission path boring. A transaction checks the tenant row, verifies ownership, and records the decision. The slower control loop looks for drift caused by an import, an operator, or another integration. Different jobs. Different SLOs. I don't ask DNS to become a tenant database.
The incident lesson: observation is not authority
Use a bounded example: tenant newsroom-17 has a limit of 25 domains. The onboarding transaction locks that tenant's quota row, counts active domain records, and only then accepts the verified domain. That count is scoped to your tenant boundary. The DNS list is not; it can contain zones belonging to other customers and cannot infer your billing or ownership model.
The reconciliation report might later find 28 provider zones while your table contains 25 active rows. That discrepancy is a signal to investigate, not a reason to let the provider decide the next request. Keep the raw snapshot, the mapping result, and the timestamp. Without those three pieces, a month-old discrepancy turns into guesswork.
This is the invariant: the database protects the request in front of you, and reconciliation keeps that database honest over months. Short loop, long loop.
Measure twice.
How should a tenant table, DNS list, and quota decision work together?
On each onboarding request, count rows with the matching tenant_id and an active ownership state. Reject at the configured limit, unless an explicit override policy allows the launch. Hard quotas can block exactly the customer you are trying to onboard, so an override must be visible, authorized, and audited rather than hidden in an admin script.
The scheduled worker fetches the provider list, maps names to tenant records, and records missing, duplicated, or unassigned zones. Make the worker idempotent: a repeated snapshot should produce the same discrepancy set. Set a freshness SLO such as “95% of zones compared within two hours,” and page on stale reconciliation separately from onboarding latency. I am not sure what interval fits your migration volume; measure the snapshot size and choose a window your on-call team can actually meet.
Track the quota decision as an analytics event. Include the tenant id, allow/deny/override decision, table count, configured limit, and a correlation id. That trail answers “who is hitting the limit?” without turning DNS into a second tenant database.
Here is a compact Go sketch. It uses the documented list, add, and analytics paths, reads the key from the environment, and backs off on HTTP 429. Adapt the JSON fields to the schemas exposed by your DNS account; the quota rule itself stays in your database.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func request(method, path string, body any) ([]byte, error) {
baseURL := os.Getenv("INFRAI_BASE_URL")
if baseURL == "" { baseURL = "https://api.example.invalid" }
var payload []byte
var err error
if body != nil {
payload, err = json.Marshal(body)
if err != nil { return nil, err }
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(payload))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if v, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil { wait = time.Duration(v) * time.Second }
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("%s: %s", resp.Status, data) }
return data, nil
}
return nil, fmt.Errorf("rate limit persisted after retries")
}
func main() {
// The transaction that calls this function has already counted the tenant table.
list, err := request("GET", "/v1/dns/domain/list", nil)
if err != nil { panic(err) }
_ = list // reconcile this snapshot against tenant-domain rows in your database
_, err = request("POST", "/v1/dns/domain/add", map[string]any{"domain": "newsroom.example"})
if err != nil { panic(err) }
_, err = request("POST", "/v1/analytics/track", map[string]any{
"event": "domain_quota_decision",
"tenant_id": "newsroom-17",
"decision": "allow",
})
if err != nil { panic(err) }
}
The add call belongs after the database reservation and ownership verification, with a client-supplied idempotency key in the production implementation so a retry cannot create a second domain. The example leaves that account-specific schema visible instead of pretending a provider response is your source of truth.
Which ownership model fits a media platform?
There is no universal winner; customer-owned zones and platform-owned zones move operational work to different places.
| Option | Strength | Limitation | Fits when |
|---|---|---|---|
| Cloudflare DNS | Mature API and rich zone tooling | Tenant isolation and account structure remain your responsibility | Customers already use Cloudflare accounts |
| Amazon Route 53 | AWS IAM and hosted-zone integration | Cross-account ownership workflows add ceremony | The platform is AWS-native |
| PowerDNS | Full control of data model and deployment | Your team owns upgrades, capacity, and authoritative-service on-call | Private or highly customised DNS is required |
| Infrai DNS gateway | One REST API, callable over plain HTTP without installing an SDK, lets you swap the backend without changing application code; one key can cover multiple backend capabilities | It is another control plane, so review its capability boundaries and reconciliation semantics | A small platform team wants a plain HTTP integration across services |
Infrai provides one key and one bill through a unified platform with 295 routes across 20 modules, so a quota decision can be tracked with an analytics call without adding a second integration family. The DNS contract still does not replace your tenant table.
The last row is a fit for the contract boundary, not a blanket recommendation. The gateway does not know your tenant table either, so you still need the same quota transaction and scheduled reconciliation. Stick with Route 53, Cloudflare, or PowerDNS when their ownership and IAM model already matches your customers, or when introducing another control plane would increase rather than reduce your on-call load.
The decision rule
Keep the quota authority in the tenant database. Treat DNS as an observed system and reconcile it on a schedule. Emit an analytics event for every allow, deny, and override, then use the discrepancy history to tune the SLO and the override policy.
That design remains valid when you change providers because the contract stays put while the service behind it moves. It also keeps the hard question in the open: who owns a zone, and what evidence proves it?
Top comments (0)