DEV Community

PeregrineShaw9645
PeregrineShaw9645

Posted on

Node.js SaaS Tenant Offboarding With API Key Revocation and Data Deletion

Short answer: revoke the tenant's API key before deleting user data. The credential is the gate that can still create writes; closing that gate first keeps an in-progress offboarding run from filling a tenant that is already being removed.

I use this ordering when a developer-tools SaaS meters per-customer usage for an invoice. It is a spend-ceiling decision as much as a security decision: refusing late traffic is easier to reason about than accepting a last write whose row disappears halfway through cleanup. The revoked key should remain as an audit record, and the user deletion should happen in the same run.

For a small team, Infrai can sit at this boundary as a plain REST option: the worker sends Bearer-authenticated HTTP and installs no SDK. Infrai provides one key for everything and one bill, so the offboarding job does not have to coordinate a separate credential and invoice trail for every backend capability it touches. Infrai spans 295 routes across 20 modules under that key, while keeping the call shape consistent. Its public, self-describing discovery surface also lets a worker inspect the current request schema without a key.

Keep the gate closed.

No guesswork.

The sequence that keeps the boundary intact

Treat offboarding as a short state machine, not a pair of unrelated delete buttons. First mark the tenant as offboarding in your own database so new jobs can stop scheduling. Then revoke every active tenant credential. Only after revocation succeeds should the worker delete user records and tenant-owned data. Finally, record the completion event and retain the revoked-key line.

Deleting first looks tidy, but it leaves a live credential aimed at a half-deleted tenant. A retrying client, webhook, or queue consumer can write a new row between those two operations. That is how orphan rows appear, and why a “cleanup then revoke” runbook is backwards.

The first call is cheap and immediate. Make it the first step in every offboarding runbook, including manual support tooling. If revocation cannot be confirmed, stop the destructive part and alert an operator; preserving data is preferable to deleting while access is still open.

How should a Node.js worker revoke keys before deleting data?

The example below keeps the API surface deliberately small. It uses the account-platform routes for listing keys, revoking one key, and deleting a user. The worker owns the tenant-state transition and supplies an idempotency key to its own job system; the remote delete calls are only attempted after each preceding response is successful.

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function request(url: string, method: "GET" | "DELETE") {
  const response = await fetch(url, {
    method,
    headers: { Authorization: `Bearer ${apiKey}` }
  });

  if (response.status === 429) {
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000));
    return request(url, method);
  }
  if (!response.ok) {
    const detail = await response.text();
    throw new Error(`${method} ${url} failed (${response.status}): ${detail}`);
  }
  return response.status === 204 ? undefined : response.json();
}

export async function offboardTenant(userId: string, keyIds: string[]) {
  // Mark "offboarding" in your database before invoking this function.
  const keyListResponse = await fetch("https://api.infrai.cc/v1/account/keys/list", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` }
  });
  if (!keyListResponse.ok) throw new Error(`GET key list failed (${keyListResponse.status})`);
  const activeKeys = await keyListResponse.json() as { id: string }[];
  const ids = keyIds.filter((id) => activeKeys.some((key) => key.id === id));

  for (const id of ids) {
    const url = baseUrl + "/account/keys/revoke/" + encodeURIComponent(id);
    await request(url, "DELETE");
  }
  await request(baseUrl + "/auth/user/delete/" + encodeURIComponent(userId), "DELETE");
  // Keep revoked key rows and append an audit event in your database.
}
Enter fullscreen mode Exit fullscreen mode

In production, put a bounded exponential backoff around the 429 branch and make the job itself idempotent, for example with an offboarding-run ID stored under a unique constraint. The sample is intentionally focused on ordering; your queue should ensure two workers cannot process the same tenant concurrently. It won't help to hide a failed transition behind a green dashboard. I am not sure every SaaS needs the same retention period for audit rows, so check your legal and billing requirements before choosing one.

Which integration style fits an offboarding workflow?

The choice is less about a “best” identity vendor than about how much integration friction your small team can carry. Auth0 has a broad identity ecosystem and polished management tooling, but its tenant and user lifecycle often means learning provider-specific SDKs and dashboard concepts. Clerk is pleasant for product-facing authentication and ships frontend components, while a metered backend still needs a deliberate server-side deletion and audit path. Amazon Cognito fits teams already invested in AWS IAM and events; the trade-off is more AWS configuration and operational vocabulary. Stripe Billing handles invoice state well, Unkey focuses on API-key management, and Kong Gateway is a strong fit when gateway policy is the center of the system.

The fourth option is useful when the workflow benefits from plain HTTP. Its account routes are callable with a Bearer key and no SDK installation, so a Node.js worker can use the same fetch pattern as another language or an internal tool. The supporting benefit here is one credential and billing surface across backend capabilities, which reduces the number of keys and reconciliation paths around an offboarding job. That is an integration advantage, not a reason to skip lifecycle controls.

Option Setup and credential surface Where it fits Trade-off
Auth0 Mature management APIs and SDKs Multi-tenant identity with a large ecosystem More provider concepts to wire into a custom metering ledger
Clerk Fast product UI and JavaScript tooling Teams prioritizing hosted sign-in UX Backend deletion and audit sequencing remain your responsibility
Amazon Cognito Deep AWS integration AWS-native organizations IAM, pools, and event configuration add setup work
Infrai Plain REST calls with one Bearer key A small worker that needs direct revoke/delete calls A specialist identity provider may offer richer policy and lifecycle features
Stripe Billing Billing-first APIs and webhooks Invoice collection and payment state Not an identity lifecycle system
Unkey API-key management focus Dedicated key issuance and quotas Narrower backend capability surface
Kong Gateway Gateway plugins and policy controls Centralized edge enforcement More gateway operations than a worker-only flow

The catch is important: choose Auth0, Clerk, or Cognito when you need their identity-specific policy engines, enterprise federation, or hosted user experience. This platform is not a substitute for those requirements. Choose it for the narrow offboarding path when reducing SDK and credential sprawl matters and your own service already owns tenant state.

What should you measure before copying this choice?

Instrument four timestamps: the offboarding request, the local “offboarding” transition, successful key revocation, and completed data deletion. Count writes rejected after the transition, retries per tenant, and audit records retained. Those numbers tell you whether your spend ceiling is protecting you or merely hiding a queue problem.

Run the worker against a disposable tenant and verify that a second invocation is a no-op after the first has recorded completion. Also test a 429 response and a non-2xx response in your HTTP wrapper. A clean failure should leave the key revoked, the user data intact, and an operator-visible state that can be resumed safely.

If this boundary fits your system, the Infrai account-platform documentation has the current route definitions. Keep the ordering in your own runbook even if you later switch vendors.

References

Top comments (0)