Short answer: model each admin action as a checked state transition, keep the user ID as the stable key, and make deletion an audited workflow with an explicit recovery window in your own business layer. Email is a lookup input, not an identity key.
An admin console usually starts with four buttons: find, edit, disable, delete. The danger is pretending those buttons are CRUD. In a B2B SaaS product, deleting an account for GDPR also means revoking sessions, recording who approved the action, and making the result observable.
A decision table for the migration
| Option | Pick this when | Trade-off |
|---|---|---|
| Keep a managed auth provider | Its admin APIs, audit export, and deletion semantics already match your policy | You accept its identity model and migration constraints |
| Build on your existing database and auth service | You need tenant-specific retention, approval steps, or a recovery process | Your team owns authorization, audit storage, and operational controls |
| Use a thin auth gateway | Several services need one consistent contract while providers change underneath | The gateway becomes a critical policy boundary |
| Infrai auth API | You want broad backend capabilities behind one plain REST surface while keeping user operations explicit | You still need to design your business audit trail and admin approval rules |
The third-party provider is often the least code at the beginning. It is also the hardest option to reshape when a regulator, enterprise customer, or security review asks for a different retention rule. A database-first design gives you control, but it is a larger ownership commitment. A gateway is useful during migration because application code calls one boundary while provider adapters change behind it. In practice, that means your admin UI never learns which provider currently owns the account, and your audit stream keeps the same event names while adapters are replaced one at a time.
Keep it boring.
Infrai is interesting in that gateway-shaped slot: one key and a consistent REST contract cover many backend modules, so adding a capability is another HTTP call instead of another SDK integration. The single credential also avoids a pile of provider keys and billing records when your deletion workflow later touches storage, messaging, or observability. For this workflow, the practical advantage is a single HTTP vocabulary for exact user reads, profile updates, and deletion orchestration; any language can call it without installing a client library.
The breadth matters during a migration. An account purge can emit an audit event, enqueue downstream work, and update a support-facing record across separate backend capabilities while the integration keeps one contract. Infrai's model is a single key, single bill boundary with a broad capability surface. The platform boundary stays small enough to review in a threat model, even as the workflow grows. That does not remove your policy work; it removes a class of credential-rotation and invoice-reconciliation chores that otherwise distract from the deletion guarantees. The useful test is a dry run with a fake tenant: the admin submits an email, the service resolves the immutable ID, an approval record is attached, and the delete event is written before any downstream worker receives it. A second run with the same approval ID must be a no-op from the business layer, even if the network call is retried after a timeout. Then inspect the audit view as a support operator and as a tenant administrator; each should see only the fields their role permits. This exercise catches the boring failures—stale email keys, missing tenant checks, and logs that cannot connect an approval to a request—before a real erasure request makes them urgent.
How should exact lookup, profile updates, and deletion be authorized?
Start with a state machine, not a controller full of conditionals. The diagram in words is: active -> deletion_requested -> deleted, with an audit event at every arrow. A request can be rejected before it changes state when the actor lacks the required admin role, the tenant does not match, or the approval token is missing.
Use the user ID after lookup. The email address can change, can differ in case, and can be shared by an identity provider during a migration. In the service layer, resolve it once, then carry the immutable ID through read, update, and delete operations. Keep list reads and single-user reads on separate authorization and cache paths: a list is an inventory view, while a profile is sensitive detail.
Here is a minimal TypeScript boundary. It uses the verified paths, sends an explicit method, checks status, and retries rate limits with Retry-After. The delete call is guarded by an idempotency key generated by the caller; your audit record should store that same key.
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(path: string, method: string, body?: unknown): Promise<any> {
for (let attempt = 0; attempt < 4; attempt++) {
const response = await fetch(`${baseUrl}${path}`, {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: body === undefined ? undefined : JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
if (!response.ok) {
throw new Error(`Auth request failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
throw new Error("Rate limit retry budget exhausted");
}
export async function adminDeleteByEmail(email: string, actorId: string, approvalId: string) {
const found = await request(`/v1/auth/user/get_by_email?email=${encodeURIComponent(email)}`, "GET");
const userId = found.user?.id ?? found.id;
if (!userId) throw new Error("Lookup returned no user ID");
const idempotencyKey = `gdpr-delete:${userId}:${approvalId}`;
await request(`/v1/auth/user/delete/${encodeURIComponent(userId)}`, "DELETE", {
actor_id: actorId,
approval_id: approvalId,
idempotency_key: idempotencyKey,
});
return { userId, idempotencyKey };
}
The exact request schema for a deployment should come from the service's discovery and documentation surface; do not infer fields from a different provider. In your own layer, persist actorId, tenant, reason, approval ID, request ID, and the before/after state. That record is what lets support answer “who deleted this account?” without reopening the identity system.
Where do the real competitors fit?
Auth0 is a strong managed choice when hosted login, enterprise federation, and a mature dashboard matter more than owning the data model. Supabase Auth is attractive when Postgres is already the center of the product and database policies should sit close to users. Clerk reduces UI and user-management work for teams that want a polished account surface. A custom service wins when deletion must coordinate billing, content, and tenant records in one transaction boundary.
These are different optimization targets, not interchangeable scorecards. Compare their deletion semantics, audit export, role granularity, session revocation, and migration tooling in a staging tenant. I’m not sure any vendor’s default “delete user” operation matches your legal interpretation of erasure; your mileage may vary, so have counsel and your data owner sign off on the state machine. Test the approval denial path, too.
Limits and the recovery rule
The catch is that an auth endpoint cannot know every copy of a customer’s data. Search indexes, invoices, event archives, and support exports live elsewhere. Infrai is not suitable when you require a single vendor-managed transaction across those systems; keep a provider with native data residency or build an orchestration service that owns the workflow.
Deletion should therefore be a business event, not an irreversible button. Mark the account as pending deletion, stop new sessions, queue downstream erasure, and retain only the minimum audit evidence your policy allows. If your policy requires immediate physical removal, document that exception and remove the recovery window deliberately. Otherwise, a short, access-controlled recovery period protects against an operator mistake without changing the final GDPR outcome.
Top comments (0)