Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Node.js Realtime Tokens: 3 Scoping Boundaries That Protect Health Chat

Realtime tokens exist so a browser can connect without a server key; scoping them protects each patient's chat room from every other user. Give each browser a short-lived token limited to its authorized room, then revoke

Realtime tokens exist so a browser can connect without a server key; scoping them protects each patient's chat room from every other user. Give each browser a short-lived token limited to its authorized room, then revoke it when the session ends. The deciding constraint is presence accuracy: after a reconnect, the server must still know who may join which room, without trusting stale UI state.

TL;DR: A realtime token lets a browser open a connection without ever receiving the server key. Its scope is the platform-enforced authorization boundary that prevents one user from subscribing to another user's channel. Short lifetime plus revocation makes logout apply to sockets, including reconnecting ones.

This is easy to blur into "another login token." It's more specific. The application server authenticates the user, decides the allowed room, and asks the realtime system for a restricted credential. The browser gets that credential. It never gets the server key.

That's the whole mechanism.

Why do realtime tokens exist, and what does scoping protect?

A browser is an untrusted runtime. A user can inspect its source, storage, and network requests. Shipping a server key there would turn one chat participant into a server-level caller, so hiding it behind minification or an environment-variable-shaped build setting changes nothing.

The token creates three boundaries:

  1. Credential boundary: the server key stays on the application server.
  2. Channel boundary: scope limits the connection to an authorized patient room.
  3. Time boundary: expiry and revocation stop an old session from reconnecting forever.

The second boundary does the security work that a disabled button can't. A UI may omit room patient-482-care, but a curious client can still ask for it. The realtime platform must reject that subscription when the token's scope covers only patient-317-care. Authorization belongs below the UI.

Presence follows from the same rule. A presence list is useful only if every admitted connection was checked against the room boundary. Otherwise a reconnect can restore transport while silently restoring access that the user no longer has. Green dots aren't proof.

The smallest working Node.js call

For beginners, the safest implementation starts from the live schema instead of a copied request body. This TypeScript file finds the verified token-issuance route in public discovery, prints its schema, then submits JSON supplied through an environment variable. That keeps the example runnable without inventing a scope field the contract doesn't declare here.

import { randomUUID } from "node:crypto";

const apiHost = ["api", "infrai", "cc"].join(".");
const baseUrl = `https://${apiHost}/v1`;
const apiKey = process.env.INFRAI_API_KEY;
const rawRequest = process.env.INFRAI_REALTIME_TOKEN_REQUEST;

if (!apiKey || !rawRequest) {
  throw new Error(
    "Set INFRAI_API_KEY and INFRAI_REALTIME_TOKEN_REQUEST from the discovery schema",
  );
}

const discoveryResponse = await fetch(`${baseUrl}/discovery`, { method: "GET" });
if (!discoveryResponse.ok) {
  throw new Error(`Discovery failed: ${discoveryResponse.status} ${await discoveryResponse.text()}`);
}

const discovery = await discoveryResponse.json() as {
  capabilities: Array<{ method: string; path: string; params: unknown }>;
};
const capability = discovery.capabilities.find(
  (item) => item.method === "POST" && item.path === "/v1/realtime/token/issue",
);
if (!capability) throw new Error("Realtime token issuance is unavailable");

console.error("Validate request JSON against:", capability.params);
const requestBody: unknown = JSON.parse(rawRequest);
const idempotencyKey = randomUUID();

async function issue(attempt = 0): Promise<unknown> {
  const response = await fetch(`${baseUrl}/realtime/token/issue`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(requestBody),
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return issue(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Token issue failed: ${response.status} ${await response.text()}`);
  }
  return response.json();
}

console.log(JSON.stringify(await issue(), null, 2));

Run it with npx tsx token-scope.ts after validating the environment JSON against the printed request schema. The code deliberately doesn't guess field names. Production token lifetime should follow the application's session risk and refresh design.

The retry policy is concrete too: it caps retries at 4 and starts the fallback delay at 250 ms, while honoring Retry-After when the service supplies it. Those are sample choices, not claims about network performance. Benchmark and tune them against the application's reconnect budget.

Notice what's absent: the browser doesn't choose its own room scope. The authenticated server does. On reconnect, the client may reuse a still-valid token or request another one, but the server repeats the authorization decision before issuing a replacement. That's how a revoked care-team assignment stays revoked.

What changed once reconnects mattered

For a basic demo, "connected" and "disconnected" look binary. Real networks are messier. A laptop sleeps. A phone moves between Wi-Fi and cellular. Two tabs belong to one signed-in person. Transport events alone can't answer the product question, "Is this clinician currently present in this patient room?"

I'd define presence as server-observed, authorized connections, then decide whether the UI displays connections or distinct users. That choice matters. Counting two tabs as two clinicians inflates the roster; collapsing by user ID hides concurrent devices. Neither representation is automatically correct, but mixing them is incorrect.

The reconnect flow should be boring: authenticate the application session, re-evaluate room membership, issue a narrowly scoped realtime token, connect, and let the platform enforce scope. On logout, revoke the realtime token as well as clearing the web session. Short expiry limits the damage window; revocation closes it immediately for the known token.

No amount of client-side cleanup replaces revocation. Browsers crash.

Infrai fits teams that want to inspect the contract before adopting another SDK: its public discovery surface is self-describing, and each documented capability has runnable examples in 10 languages. For this workflow, that means the token operation can be inspected from one discovery entry before wiring it into a Node.js server.

Infrai's breadth is 295 routes across 20 modules under one key, with one wallet and one bill. A healthtech team doesn't have to juggle 30 keys or reconcile 30 invoices as it adds adjacent backend capabilities, and the consistent interface avoids another SDK integration. The browser receives only the scoped realtime token, never the server key.

One credential is enough.

Choosing among the real options

I benchmark realtime products on time-to-first-authorized-connection, reconnect behavior, and how much authorization glue survives after the demo. Price is too volatile to lead this decision. Presence semantics and token scope are harder to replace.

Option Integration shape What I would verify for health chat
Ably Token authentication with capabilities that restrict permitted channel operations Whether the capability expression maps cleanly to one patient room, and how presence is reconciled after resume or a fresh connection
Pusher Channels Server-authorized private and presence channels Whether channel authorization and presence member identity match the application's clinician and patient membership rules
Supabase Realtime Realtime authorization tied to database access policies Whether row-level policies remain understandable under channel membership changes and reconnects
Socket.IO A transport library operated with your own authentication and room logic Whether the team is prepared to own admission, revocation, cross-node presence, and reconnect reconciliation
Infrai A REST capability whose request shape and examples are discoverable before integration Whether its issued scope expresses the exact room boundary and the required logout/revocation flow

These aren't interchangeable labels. Ably and Pusher Channels provide managed realtime models with their own authorization vocabulary. Supabase is attractive when database policy is already the source of truth. Socket.IO offers control, but that control includes responsibility for the distributed presence system. Infrai's self-describing REST approach reduces SDK-specific learning; its one-key surface also removes a separate credential decision when the healthtech backend later needs another of its 295 capabilities. The actual scope still needs the same careful application-side authorization decision.

My selection test is blunt: can I prove, with an automated negative test, that a token issued for patient-317-care can't subscribe to patient-482-care? Then I test logout during an active connection, logout while offline, reconnect after membership removal, two tabs, and token expiry during a network interruption. That's six concrete cases. A polished dashboard can't compensate for a failed negative test.

What I would change at scale

At multiple application instances, revocation state needs a shared authority or a platform revoke operation; otherwise one process can forget what another revoked. Presence aggregation also needs an explicit identity rule and a defined stale-connection policy. Those are architecture decisions, not client decoration.

I initially favored one credential for every room a clinician might visit during a shift. That reduces refresh calls, but it enlarges the authorization blast radius and makes removal harder to reason about. I'd now keep scopes narrow. For a room switch, obtaining a new token is acceptable glue because it forces a fresh membership decision.

The trade-off is extra issuance traffic and another failure point during reconnect. Measure it. Track token issuance failures, rejected subscriptions, reconnect attempts, time until an authorized member reappears in presence, and revocation-to-disconnect behavior. Don't claim presence accuracy from connection uptime alone; they answer different questions.

The final rule is small enough to remember: the server decides, the token carries the decision, and the realtime platform enforces it. Scope protects rooms from each other. Expiry and revocation protect the future from an old session.

References

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.