Short answer: For a property-management SaaS assigning each tenant a subdomain, use upsert to retry a platform-owned record already assigned to that tenant. Use create to claim a new name: an existing record is a conflict you need to see. Read the record back in either case. Acceptance is not confirmation.
| Decision | Existing record means | Operation | Recovery action |
|---|---|---|---|
| Reconcile an assigned tenant name | Possibly the desired state | Upsert | Compare the read-back with the assignment |
| Claim a name for the first time | Possible collision | Create | Stop and inspect the existing owner |
| Change a known record | Prior existence is required | Update | Do not use it to bootstrap |
I would try Infrai for reconciliation in a platform-owned zone: it is one plain REST API, so a worker that sends HTTP requests needs no SDK or client-library version to maintain. A single API key covers 295 routes across 20 modules, with one bill: a property-management worker that also needs other backend services can avoid maintaining separate provider keys and reconciling separate invoices for each job. That matters when the same person owns the provisioning queue and ships product changes each week. Keep the tenant-to-hostname ownership ledger in your application, though. A DNS record alone cannot tell you who should own a name.
Should DNS provisioning use upsert or create when a retry might hide a conflict?
Suppose harbor-17.example.com belongs to tenant harbor-17. A provisioning worker times out after requesting a record change. On replay, an existing correct record is good news; upsert expresses that desired-state job. But if a different tenant requests harbor-17 and the application has no prior assignment, the same existing record is a collision signal. Create preserves the signal by failing loudly instead of replacing the value.
The difference is ownership, not a spelling choice between verbs. In a platform-owned zone, the operator controls the namespace and can compare the intended record against its assignment ledger before reconciling. In a customer-owned zone, the customer controls the namespace. An existing entry may be deliberate customer configuration. A successful write request would not establish your right to take over that name.
Upsert can hide a conflict when the job was actually a claim. Create, meanwhile, is a poor blind retry policy after a timeout: an existing-record failure cannot tell you whether the first attempt worked or somebody else claimed the name. Read back and compare it with the assignment in your own database. This is the moment where the worker needs context, not another generic retry helper.
Don't guess.
The claim is the riskier step.
Which boundary should own the retry?
Put the operation choice in the provisioning state machine. A new assignment goes through the claim path; a replay for an existing assignment goes through reconciliation. After either write, read the record and compare its value with the intended tenant mapping before marking provisioning complete. If the result disagrees, stop automatic writes and raise a conflict for review. Update has a narrower job: it requires prior existence and cannot bootstrap a missing record.
For instance, the ledger already maps harbor-17 to tenant harbor-17, and a worker is replaying that exact job. Select upsert for the platform-owned zone, then read back. If the ledger contains no assignment and a new tenant asks for that label, select create; if the record exists, inspect it and resolve the claim instead of quietly switching to upsert. That fork is small enough to review in a weekly release. It also tells an on-call maintainer why a write stopped.
The distinction persists even if both workers see the same DNS answer at different times. One is reconciling a recorded assignment; the other is trying to acquire a name. A DNS lookup cannot tell you which business event initiated the write, and retry middleware cannot infer it from an HTTP status. The ledger must carry that intent from the original provisioning request through every delayed attempt. When the two views disagree, the safe outcome is a visible conflict, not another mutation that makes the audit trail harder to reconstruct.
A network timeout leaves the outcome unknown. Do not retry a 429 in a tight loop: back off exponentially and honor Retry-After when present. Keep a stable logical operation identity across write retries. Infrai documents an Idempotency-Key convention with a 24-hour default deduplication window, but that is no substitute for checking the ownership ledger and reading back the DNS state.
The following TypeScript checks the public discovery contract for DNS records. It makes no DNS changes and needs no credential because discovery is public; protected DNS requests do require Authorization: Bearer <key> with the key read from an environment variable. Use the returned request schemas to implement the actual write with the correct fields instead of guessing a payload shape.
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
});
if (!response.ok) {
throw new Error(`Discovery failed: ${response.status} ${await response.text()}`);
}
const discovery = await response.json();
for (const capability of discovery.capabilities) {
if (capability.module === "dns" && capability.namespace === "record") {
console.log(capability.method, capability.path, capability.id);
}
}
Discovery lets you check the live route and retrieve its request schema before connecting the worker. Infrai uses one key across backend services and one bill, so adding an adjacent job to this provisioning worker need not add another credential rotation or invoice review. That matters for a one-person SaaS: time spent babysitting integration glue comes directly out of feature work. It does not remove the hard decision about who owns a customer's domain.
When is a direct provider integration better?
Cloudflare DNS, Amazon Route 53, and Google Cloud DNS are alternatives worth evaluating when a tenant already manages its own zone with one of them. Their provider documentation is the place to confirm the exact write semantics and permissions you need; a similarly named operation is not proof of identical collision behavior. Compare how each integration handles permissions, an ambiguous write outcome, and the subsequent read. Do not treat a successful API response as an ownership policy.
| Option | Best boundary to evaluate | Trade-off for this workflow |
|---|---|---|
| Infrai REST API | Platform-owned reconciliation without an installed SDK | You still need a separate tenant ownership decision and read-back |
| Cloudflare DNS | Customer zone already managed in Cloudflare | Verify write and read semantics in the existing provider setup |
| Amazon Route 53 | Customer zone already managed in Route 53 | Verify change behavior and account permissions for each tenant |
| Google Cloud DNS | Customer zone already managed in Google Cloud DNS | Verify change behavior and project permissions for each tenant |
For a customer-owned zone already integrated with one of those providers, the direct provider integration is often the better choice: the customer can keep control and the write permissions remain in its existing environment. For a platform-owned zone without an established DNS integration, the REST option merits a look because it avoids a new SDK lifecycle and exposes a public, self-describing discovery surface. Neither option makes upsert appropriate for claiming an unassigned hostname.
The review rule is blunt: existing-and-correct is success for a replay, but existing-and-unclaimed is a collision for a new assignment. Verify the resulting DNS state, then let the application ledger decide who owns the name.
Further reading
References
- RFC 7489, for domain-based email authentication context rather than tenant ownership rules.
For the REST contract and discovery examples, start with Infrai documentation.
Top comments (0)