DEV Community

KendrickBerg5327
KendrickBerg5327

Posted on

Revoke Realtime Access When a Session Ends — 3 Node.js API Checks

Close the property-management session in the authoritative store before telling any viewer that the live poll has ended. Then reject new votes and reconnects against that state, stop delivery to existing connections, and check that queued messages cannot restore access. Short answer: a socket disconnect alone is not revocation. For a Node.js service with many viewers, choose the API design by its token scope and its ability to enforce the session boundary after reconnect, not by its advertised connection count or price.

The failure to plan for is a tenant who leaves the meeting page open while a property manager closes a poll on a proposed building repair. Another tab reconnects using a credential issued while the poll was open; a delayed vote arrives behind the close event. If the server only broadcasts "closed," the browser becomes the authority for a decision it should not make. This is an ordering problem as much as an authentication problem.

The close must win.

How should an API revoke realtime access when a session ends?

Separate three permissions: reading poll updates, submitting a vote, and joining the session again. A tenant viewing the result may legitimately keep read access after voting stops; a tenant from a different building should never acquire it. Decide the policy before implementation. A token scoped to a building but valid across all meetings has a very different blast radius from one scoped to a single poll and session. Neither is equivalent to the server checking whether that poll is still open.

Treat the server's session status as the source of truth. A signed token establishes a subject and a bounded capability; an expiration time limits how long an unrevoked credential may be reused, but does not itself invalidate a credential when the session closes. RFC 7519 defines the exp claim as the time on or after which a JWT must not be accepted. For immediate closure, a reconnect must consult current authorization state or a version of it that the server can invalidate. The same check belongs on each mutation, because a connection that was allowed to join ten minutes ago is not proof that its next vote is allowed now.

Two checks, two clocks. Token expiry follows credential time; poll closure follows the authoritative session transition. If an API checks only expiry at handshake, a client with an unexpired token can reconnect after closing time. If it checks session status only at handshake, a client already connected can still send a vote after closure. The write path needs the durable gate even when the transport keeps its connection open, and the subscription path needs an explicit policy for what may still be read. This distinction matters most when many viewers reconnect together: the number of sockets changes the load, not the definition of authorized access.

Be precise about the boundary. Stopping outbound updates on an already open connection may require removing its subscriptions or closing that connection, depending on the transport. WebSocket defines a closing handshake, but a close frame is not a transaction that reverses an accepted vote. WebRTC's peer connection model is relevant if the session carries media or data channels; it does not make application-level poll authorization disappear. Keep the access decision in the application service.

No client-side timer can replace that decision.

Put the close ahead of the broadcast

Use a monotonic session state transition: open to closed, with a close time and a generation or revision recorded alongside it. Make the close command idempotent. Retried jobs should observe closed and return the same outcome, not reopen the poll or enqueue a second round of work. The event sent to viewers describes a committed transition; it must not be the transition itself.

For a vote, enforce membership and poll status inside the same durable operation that records the vote. A read of open followed by a separate write leaves a race with closure. A conditional update or transaction is the useful primitive here; the exact SQL depends on the schema. Give each vote attempt an idempotency key scoped to the voter and poll so a retry after a timeout cannot count twice. If the policy allows one vote per tenant, a unique constraint should enforce that invariant as well. Do not rely on message ordering across independent worker processes to settle a ballot.

Retries happen.

Node.js can express the gate without trusting the browser's notion of time. This sketch deliberately leaves persistence and token verification behind interfaces: those implementations must bind the tenant to the building, check the session revision, and make recordIfOpen atomic.

type PollStore interface {
    RecordIfOpen(ctx context.Context, sessionID, pollID, tenantID, key string, choice int) (bool, error)
    CloseIfOpen(ctx context.Context, sessionID string) (bool, error)
}

func submitVote(ctx context.Context, store PollStore, a Claims, pollID, key string, choice int) error {
    if a.SessionID == "" || a.TenantID == "" || key == "" {
        return ErrUnauthorized
    }
    accepted, err := store.RecordIfOpen(ctx, a.SessionID, pollID, a.TenantID, key, choice)
    if err != nil {
        return err
    }
    if !accepted {
        return ErrPollClosed
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The Go interface illustrates the invariant even if the surrounding API is Node.js: authorization claims are validated at the edge, while acceptance is decided where the durable state lives. A real handler must distinguish an already-recorded idempotency key from a rejected vote; returning ErrPollClosed for a previously committed retry can confuse clients about whether their ballot counted. Expose an explicit accepted, duplicate, or closed result from storage once that contract is designed.

For token issuance, grant only the specific poll actions the client needs. Avoid putting a long-lived signing secret in the browser, and do not accept client-supplied building or tenant identifiers as proof of membership. On reconnect, validate the credential, its scope, the current session state, and the viewer's membership. If a token is short-lived, document the maximum residual read access under the chosen enforcement model. Where immediate read revocation is required, expiration alone is insufficient: the server must terminate or reauthorize active subscriptions too.

Check the shutdown path under load

Before shipping, race a vote against the close operation and assert that the durable order determines the outcome. Repeat a vote with the same idempotency key after a simulated lost response. Reconnect with a previously valid token after close. Try a token for another building. Finally, leave a viewer connected while the close event is delayed or dropped and verify that its next attempted write is refused. Those tests exercise the trust boundary, not just the happy-path UI.

Observe the system at that boundary: count rejected post-close writes, reconnect denials, duplicate keys, and the interval between the committed close and the last authorized subscription. Keep the poll/session identifiers in structured logs but avoid logging raw tokens or ballot contents. A surge in denied reconnects after a close can be normal; accepted writes after that close are an incident. Page on the invariant, not merely on socket disconnect volume.

Deploy the enforcement check before changing token scope or the viewer UI. During a rolling release, an older worker that accepts writes based only on connection state can defeat the new rule, so first confirm every write path uses the same conditional storage operation. If a release misbehaves, roll back presentation or connection handling without reopening closed polls; retain the durable close gate and replay only events that represent committed state. Write that sequence in the runbook while it is still calm.

There is a cost trade-off, but it comes after correctness. A per-message authoritative read may load the database heavily with many viewers, while a cached session revision reduces reads at the price of an explicit invalidation and bounded propagation delay. Reads of public aggregate results may tolerate that delay; acceptance of votes usually cannot. Measure the actual close-to-denial interval and choose the cache policy against the required revocation window. No connection pricing model resolves that decision for you.

References

Top comments (0)