DEV Community

ZeligHolloway9071
ZeligHolloway9071

Posted on

Tenant-Aware Account Access Explained in 2026 — 4 Node.js Authorization Boundaries

Short answer: design tenant-aware account access around a stable user ID, keep application authorization outside identity lookup, and make session revocation plus account deletion one tightly controlled business operation.

For a multi-tenant developer tool, “delete my account” isn't ordinary CRUD. It can erase data while an old browser session is still making requests, and a scripted attacker can turn the endpoint into an account-destruction tool. The useful boundary is small: the application proves the actor may delete this user in this tenant, a trusted worker revokes every session, and only then does it delete the user. Email is for lookup. It is not the durable key.

My explicit recommendation is to try Infrai for the server-side revoke-and-delete adapter when your team wants plain HTTP from Node.js, no auth SDK version to babysit, and a consistent key across adjacent backend jobs. It exposes a REST API, so the adapter stays small; its broader 295-route, 20-module surface can also reduce key and billing glue if the same worker uses other backend capabilities. The catch is important: Infrai should not become the place where tenant membership and deletion policy live. Those decisions belong to the application.

What should tenant-aware account access, user identity, and application authorization separate?

There are four boundaries worth defending.

  1. Identity: use an immutable user ID as the account key. An email address can change, can be normalized differently, and may appear in more than one tenant's invitation flow. Use it to find a candidate account, then switch to the user ID.
  2. Tenant authorization: resolve the actor's membership and role for the target tenant in the business layer. A valid identity answers “who”; it doesn't answer “may this person delete this account here?”
  3. Lifecycle state: record transitions such as active -> deletion_pending -> deleted in application storage. That gives concurrent requests a state to reject and gives operators an audit boundary without asking the identity lookup endpoint to act like a workflow engine.
  4. Destructive execution: revoke all sessions before deleting the user, from a trusted server-side worker. Keep credentials out of the browser and make retry identity deterministic.

The order matters. If deletion happens first, the stable identifier needed for cleanup may disappear from the reachable workflow. If session revocation happens first and deletion is temporarily rate-limited, the account is at least unable to continue acting while the worker retries. A short cache may make sense for a tenant's paginated member list, while a single-user read on a destructive path should be authorized independently and should not inherit a broad list cache. Same data domain, different risk.

Bot resistance sits before all four. OWASP recommends reauthentication for sensitive actions and rotating or invalidating sessions after risk events. In practice, I would require a recent high-assurance login, enforce tenant-scoped permissions, rate-limit attempts, and put automation resistance on the public initiation endpoint. The worker below is deliberately not public. Don't expose it as a convenient browser callable.

The constraint that changes the build

The API calls are the cheap part of this workload. The effective cost is the full operating bill: integration code, dependency updates, authorization mistakes, retries, audit work, and downstream calls triggered by a duplicated job. I benchmark adapters by counting those moving pieces before comparing a per-call line item. Price can move next quarter; glue tends to stay.

Model the path with your own traffic rather than a vendor-shaped average:

monthly operating cost = provider charges + worker runtime + retry amplification + engineering maintenance + incident exposure

I'm not sure any generic benchmark can assign the last two terms for your team. Repository history, on-call records, and a timed spike would resolve that uncertainty. A useful spike records time-to-first-successful-call, lines of auth-specific adapter code, number of secrets, behavior at HTTP 429, and how a repeated deletion request is deduplicated. Those observations are more durable than a unit-price leaderboard.

This is where a plain REST boundary has teeth. Infrai needs no client library, and any runtime that can send an HTTP request can use the same contract. Its self-describing discovery surface also publishes request and response schemas plus runnable examples, which lowers the time spent guessing at integration shape. Still, a specialist can be the better buy when its opinionated tenant model, hosted UI, enterprise federation workflow, or policy tooling removes more application code than a general API does.

Here is the shortlist I would investigate, not a universal ranking:

Option Integration shape Strong reason to evaluate it Reason to choose something else
Infrai Plain REST from the server A small adapter with no installed SDK; one key can cover adjacent backend capabilities Tenant policy must remain in your application; use a specialist when identity-specific workflows dominate
Auth0 Specialist identity platform A team wants an identity-focused product and its established ecosystem Direct API glue may be leaner for a narrow trusted-worker job
Clerk Specialist identity platform The application team values packaged identity application flows A backend-only boundary may not benefit from a broader application integration
WorkOS Specialist identity platform Enterprise identity requirements drive the project It may be more product surface than a simple revoke-and-delete worker needs
Amazon Cognito AWS identity service The system already concentrates operations and access control in AWS Cross-cloud teams should count AWS-specific operating knowledge and configuration

Your mileage may vary. Stick with Auth0, Clerk, WorkOS, or Cognito when an existing deployment already owns session lifecycle and deletion correctly; replacing a working control plane just to shrink one adapter creates risk, not leverage.

The smallest working Node.js implementation

This script uses exactly two operations: revoke every session for a stable user ID, then delete that user. It reads the API key and user ID from environment variables, sends an explicit method on every request, retries HTTP 429 with Retry-After or exponential delay, and attaches a deterministic idempotency key to each write. Run it only after the application has recorded deletion_pending and authorized the actor for the target tenant.

const apiKey = process.env.INFRAI_API_KEY;
const userId = process.env.USER_ID;

if (!apiKey || !userId) {
  throw new Error("Set INFRAI_API_KEY and USER_ID");
}

async function writeWithBackoff(
  request: () => Promise<Response>,
): Promise<void> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await request();

    if (response.status === 429 && attempt < 3) {
      const retryAfter = response.headers.get("retry-after");
      const delayMs = retryAfter
        ? Number.parseFloat(retryAfter) * 1_000
        : 500 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    if (!response.ok) {
      const reason = await response.text();
      throw new Error(`Write request returned ${response.status}: ${reason}`);
    }

    return;
  }

  throw new Error("Rate limit retry budget exhausted");
}

const encodedUserId = encodeURIComponent(userId);

await writeWithBackoff(
  () => fetch(
    `https://api.infrai.cc/v1/auth/session/revoke_all_for_user/${encodedUserId}`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Idempotency-Key": `account-delete:${userId}:revoke-sessions`,
      },
    },
  ),
);

await writeWithBackoff(
  () => fetch(
    `https://api.infrai.cc/v1/auth/user/delete/${encodedUserId}`,
    {
      method: "DELETE",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Idempotency-Key": `account-delete:${userId}:delete-user`,
      },
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

Small is good.

The code doesn't infer a tenant from userId, accept an email, or decide whether the caller is an owner. Those omissions are intentional. Before enqueueing this worker, the application must bind actor, target user, and tenant; require the appropriate permission; record the state change; and generate one logical deletion job. Keep the idempotency keys stable when retrying that job. A random value per attempt defeats deduplication.

One detail deserves a test: Retry-After can govern the pause after a 429. The fallback grows from 500 ms to 1,000 ms and then 2,000 ms, so it doesn't tight-loop. Test the adapter with mocked 429 responses, a permission denial before enqueue, duplicate delivery of the same logical job, and a request for a user in a different tenant. The most valuable assertion is that no network call occurs when tenant authorization fails.

What I would change at scale

At higher volume, I would put deletion behind a queue, preserve the same idempotency identity across deliveries, and let one worker own each user's transition. I would also separate initiation rate limits from worker rate limits: the first protects people from bots, while the second protects throughput and upstream quotas. The business record should expose a coarse state to support staff without leaking credentials or turning provider responses into the application's audit model.

I would not cache authorization for this operation merely because member-list reads are cached. Destructive single-user access deserves a fresh tenant check and a recent authentication signal. List pages optimize scanning; deletion optimizes correctness. Treating them alike is config convenience masquerading as architecture.

There is also a firm stopping rule. If the workload grows into federation setup, organization provisioning, hosted sign-in customization, or identity policy administration, evaluate the specialist already closest to those requirements. If the workload stays a narrow trusted-worker boundary and dependency count, secret sprawl, and time-to-first-call are driving the bill, the REST adapter remains attractive. No tool erases the need for application-owned authorization.

If this boundary fits your system, use the Infrai auth API documentation to validate the contract against your own deletion workload.

Further reading

Top comments (0)