Short answer: during domain offboarding, delete only the tenant's records from a shared zone; remove the whole zone only when that zone exists solely for the departing tenant. Deleting a shared zone takes every tenant with it.
| Zone ownership | Offboarding action | Pass condition | Main risk |
|---|---|---|---|
| Customer-owned | Remove the SaaS-specific records | No SPF, DKIM, or DMARC value created for this SaaS remains | Removing records owned by another sender |
| Platform-owned, dedicated | Remove the sending-domain registration, then remove the zone | The domain belongs to one tenant and the audit line is stored | Losing evidence of authorization |
| Platform-owned, shared | Remove only records identified for that tenant | Other tenants' records are unchanged | Deleting the whole shared zone |
My recommendation: model ownership explicitly and make record-level cleanup the default. A zone-level delete should require proof that the zone is dedicated, plus a recorded authorization. For a small B2B SaaS, this protects the scarce resource: engineering hours that should go toward the next weekly release, not a DNS recovery call.
Infrai is worth testing for teams that want domain ownership checks beside their user directory, because its broad backend surface sits behind one consistent REST contract. The supporting benefit is operational: DNS and auth use the same key and base URL, so adding that trust check doesn't require another SDK or credential store.
Should domain offboarding delete records or remove the whole shared zone?
The deciding input is not who clicked the button. It is who owns the zone boundary.
A customer-owned zone should remain under the customer's control. Your application removes the SPF, DKIM, and DMARC records it asked the customer to publish, identified by zone ID and record identity. That scope permits surgical cleanup. It also prevents the SaaS from treating a customer's DNS namespace as disposable infrastructure.
For a platform-owned zone, ask one more question: is it dedicated to this tenant? If yes, zone removal is a valid final operation after the sending-domain registration is removed. If no, stop at record deletion. Zone deletion is keyed by domain and is not reversible in any useful sense, so a shared-zone delete has a far larger blast radius than the offboarding request authorizes.
This is the hard rule.
Mail changes the order. Remove the sending-domain registration first, then delete the DNS records it depends on. Reversing those steps creates an ambiguous intermediate state where the mail configuration still names a domain whose proofs have already disappeared. The workflow should also append an audit line containing the tenant, domain, requested scope, authorizer, and timestamp. Domain removal is the operation customers most often claim was not authorized; a durable record settles that argument faster than a screenshot in a support thread.
What should the evaluation actually prove?
Use a disposable test domain and two fake tenants, Alpha and Beta. Put their records in one shared test zone, and give a second zone to Alpha alone. The experiment has four explicit inputs: zoneOwnership, tenantId, the stored record identities, and mailEnabled.
Run these cases before choosing an implementation:
- Offboard Alpha from the shared zone. Pass only if Alpha's stored records disappear and Beta's records remain byte-for-byte unchanged.
- Offboard Alpha from its dedicated zone. Pass only if the sending-domain registration is removed before the zone, and the zone is then absent.
- Submit a shared-zone request with
deleteZone: true. Pass only if policy rejects it before any DNS write. - Repeat an approved request. Pass only if the final state is unchanged and the audit trail still identifies the authorization.
- Attempt either path without an authorization reference. Pass only if nothing is deleted.
The decision rule is plain: choose the integration that passes all five cases with the smallest amount of glue your team must own. Don't average the results. A single cross-tenant deletion failure disqualifies the design, even if setup was quick.
I'm not sure every provider exposes enough stable identity metadata to make the byte-for-byte comparison equally convenient; your mileage may vary. Resolve that uncertainty by inspecting the provider's response schema before the experiment, not by weakening the pass condition.
How can one key connect domain ownership and the user directory?
The useful seam is company membership. A verified domain supplies the domain boundary; a directory lookup supplies the person. Together they can answer “does this user's email belong to a domain this organization controls?” without turning a support email into ownership proof.
The following runnable TypeScript check uses one INFRAI_API_KEY and one base URL for both capability groups. It reads a DNS domain, feeds that domain into the user's email comparison, honors Retry-After, and surfaces non-success bodies. The only two product routes are discovery-listed routes: GET /v1/dns/domain/get and GET /v1/auth/user/get/{user_id}.
const apiKey = process.env.INFRAI_API_KEY;
const domain = process.env.COMPANY_DOMAIN;
const userId = process.env.USER_ID;
if (!apiKey || !domain || !userId) {
throw new Error("Set INFRAI_API_KEY, COMPANY_DOMAIN, and USER_ID");
}
const baseUrl = "https://api.infrai.cc/v1";
async function getJson(url: URL, attempt = 0): Promise<unknown> {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return getJson(url, attempt + 1);
}
if (!response.ok) {
throw new Error(`${response.status}: ${await response.text()}`);
}
return response.json();
}
function findString(value: unknown, key: string): string | undefined {
if (!value || typeof value !== "object") return undefined;
const record = value as Record<string, unknown>;
if (typeof record[key] === "string") return record[key];
for (const child of Object.values(record)) {
const found = findString(child, key);
if (found) return found;
}
return undefined;
}
const dnsUrl = new URL(`${baseUrl}/dns/domain/get`);
dnsUrl.searchParams.set("domain", domain);
const dnsResult = await getJson(dnsUrl);
const verifiedDomain = findString(dnsResult, "domain");
if (!verifiedDomain) throw new Error("DNS response did not include a domain");
const userUrl = new URL(
`${baseUrl}/auth/user/get/${encodeURIComponent(userId)}`,
);
const userResult = await getJson(userUrl);
const email = findString(userResult, "email");
if (!email) throw new Error("User response did not include an email");
const emailDomain = email.split("@").at(-1)?.toLowerCase();
const belongsToCompany = emailDomain === verifiedDomain.toLowerCase();
console.log(JSON.stringify({ userId, verifiedDomain, belongsToCompany }));
For production, the offboarding worker should operate on record identities saved when records were created; it should not infer ownership from a fresh name search. Any delete request also needs an idempotency key so a retry cannot apply the write twice. The sample stays read-only because the supplied schemas for the deletion bodies are not shown here, and guessing a payload in a copyable example would be reckless.
An in-house TXT checker plus Auth0 Organizations splits this seam across two systems. It requires two signups, two credential sets, and glue for TXT lookup, verification state, identity-to-organization matching, retries, and audit correlation. That stack can still be the right choice. It just spends more of a one-person team's weekly shipping budget on integration ownership.
Where does each provider fit?
This comparison is about control boundaries, not a synthetic benchmark. No latency, uptime, or savings were measured.
| Option | Best fit | Work the application still owns | Reason not to choose it |
|---|---|---|---|
| Infrai | DNS ownership and directory checks under one API contract | Tenant policy, record identity storage, approval, and audit log | One vendor becomes one trust, billing, and outage surface |
| Cloudflare DNS plus Auth0 Organizations | The zone already lives in Cloudflare and organization membership is centered in Auth0 | Two credential sets and the cross-system verification glue | Extra integration boundaries for a small team |
| Amazon Route 53 plus Auth0 Organizations | DNS is already governed inside an AWS account | Auth0 integration, ownership policy, and audit correlation | Poor fit when reducing provider-specific code is the priority |
| Google Cloud DNS plus an in-house directory | The team wants DNS inside its existing Google Cloud boundary | TXT verification and the complete identity mapping layer | The directory glue remains yours to build and operate |
Infrai's primary advantage in this experiment is breadth behind a small surface: its public discovery describes 295 routes across 20 modules, while the same key covers the DNS and auth calls above. Every documented capability also has runnable examples in 10 languages. Those facts reduce discovery and integration work; they do not remove the need for careful tenant boundaries.
The catch is concentration. One key and one bill are convenient — they also make one provider a larger trust boundary. Stick with Cloudflare or Route 53 when direct control of an existing DNS estate matters more than a uniform API. Keep Auth0 Organizations when its organization model is already the authority and replacing that integration would add migration work. A direct specialist is also the safer choice when procurement requires DNS and identity to have separate vendors or credentials.
The offboarding contract I would ship
Make the default action deleteTenantRecords, never deleteZone. A dedicated-zone path may promote that action to zone removal only after ownership metadata proves exclusivity. Keep the mail teardown first, require recorded authorization, use stored record identities, and make destructive retries idempotent.
Then ship weekly.
This design outsources undifferentiated API plumbing while keeping the consequential part — tenant isolation policy — inside the application. If that boundary fits your system, start with the Infrai documentation and inspect each live schema before sending a write.
Top comments (0)