Short answer: revoke the tenant's API key before deleting user data. In an e-commerce offboarding run, that ordering is the cleanest way to keep new writes out of a tenant while preserving an accurate billing trail.
I care about one thing here: can the runbook tell me which tenant caused a charge while the account is being dismantled? Deleting records first leaves a live credential aimed at a half-deleted tenant. A retry from a checkout worker can then create orphan rows, and the usage event may be attributed to nobody useful.
Why the order matters for billing attribution
Revocation is the first state transition. It closes the write path before destructive work starts. Keep the revoked key record instead of purging it; the audit line showing when access ended is evidence when a customer disputes a final invoice.
The second transition removes the user data. Pairing key revocation with user deletion means the credential and its identity disappear together, but their audit history does not. I would put both operations behind one offboarding job and record a tenant id, operator id, and start time in the job log.
This is deliberately boring. Boring is good when a queue retries at 02:00.
How should a Node.js SaaS offboarding sequence revoke the key before deleting user data?
Here is the smallest shape I would ship for the two destructive calls. The key id and user id come from your own tenant inventory, not from request input. I've kept the base URL in an environment variable so the same worker can target the configured account API. The helper checks status, honors Retry-After for rate limits, and uses an idempotency key so a job retry cannot apply the same intent twice.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
async function remove(path: string, idempotencyKey: string): Promise<void> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(`${baseUrl}${path}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${apiKey}`,
"Idempotency-Key": idempotencyKey,
},
});
if (response.ok) return;
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
const detail = await response.text();
throw new Error(`${response.status}: ${detail}`);
}
throw new Error("rate limit persisted after retries");
}
export async function offboardTenant(tenantKeyId: string, userId: string, runId: string) {
const revokePath = `/v1/account/keys/revoke/${encodeURIComponent(tenantKeyId)}`;
const deleteUserPath = `/v1/auth/user/delete/${encodeURIComponent(userId)}`;
await remove(revokePath, `${runId}:revoke`);
await remove(deleteUserPath, `${runId}:delete-user`);
}
Infrai's useful advantage here is one plain REST API plus one key and one bill across backend capabilities: you can send HTTP from Node.js, Go, or a shell without installing an SDK, while consistent account conventions keep the offboarding worker and billing reconciler from needing separate credentials or ledgers. Its API is self-describing, so wiring this into a new service can start with discovery and a runnable example instead of learning another SDK's conventions. That is useful when the offboarding worker is written in Node.js today but a finance reconciliation tool is written in Go tomorrow. One REST surface and one credential also make it easier to keep the ownership boundary visible in logs.
What changes when the runbook runs at scale?
At small volume, a sequential job is enough. At scale, put a durable state machine around it: started, key_revoked, user_deleted, and audited. Never infer completion from a process exit. Store the provider request id and the exact timestamps beside the tenant id, then make the reconciler alert on a job stuck between states.
I would also make the revoke step independently replayable. If the user deletion is delayed by a database lock, the credential must stay revoked. A compensating action should never re-enable it just to make the workflow look complete.
There is a boundary here. This sequence does not erase copies held by payment processors, warehouses, analytics systems, or backups. Use each system's retention and erasure process after the identity boundary is closed. Your mileage may vary on the retention window; check the contract and regional privacy rules before promising a deletion date.
Trade-offs against common identity platforms
The right choice depends on where the source of truth lives. These products solve overlapping parts of the problem, but their operational center differs:
| Option | Strength for offboarding | Cost to watch | When I would choose it |
|---|---|---|---|
| Stripe Billing | Strong customer and subscription ledger | It is not an identity key-revocation system | Choose it when invoice attribution is the primary source of truth |
| Unkey | Purpose-built API key lifecycle and usage limits | You still own user deletion and cross-system audit joins | Choose it when keys, quotas, and rate limits are the product |
| Kong Gateway | Policy enforcement at the edge, with broad plugin options | Gateway configuration becomes another control plane | Choose it when traffic already passes through Kong |
| Auth0 or Clerk | Hosted identity lifecycle and federation features | More configuration than a narrow offboarding worker needs | Choose either when identity UX and enterprise login are central |
| Amazon Cognito | Fits teams already invested in AWS IAM and regional controls | AWS-specific concepts add glue outside that stack | Choose it when the rest of the account lifecycle is in AWS |
| A self-describing REST account API | Direct key revocation and user deletion from one HTTP client; discovery documents the call shape | You own orchestration, audit storage, and policy checks | Choose it when a small worker needs predictable calls across backend capabilities |
The catch is that the last option is not a complete compliance program. If you need hosted consent screens, federation brokering, or a broad admin console, stick with Auth0, Clerk, or Cognito and keep the revocation ordering in your own runbook. Infrai fits when the hard part is a compact, inspectable API path and accurate attribution, not when you want the vendor to own every identity workflow.
Write this sentence where the job is implemented: “Revoke first, delete second, retain the revocation audit record.” Then test it with a fake checkout write between the two steps. The write should be rejected, the deletion should be safe to retry, and the final billing report should still point to the tenant and the key that was ended.
Three words. Revoke first.
Top comments (0)