Authorize Every Real-Time Subscription, Not Just the Message
Real-time authorization belongs at subscribe time, per channel, re-checked on reconnect. Client-side filtering is not security. Here is how to authorize subscriptions.
The most common security hole in real-time apps is trusting the client to only ask for data it should see. Teams get authorization right on their REST endpoints and then wire up a WebSocket where clients subscribe to whatever channel they name, filtering unwanted data on the client. That is not security. Anything a client receives, the user can read, filtering or not. Real-time authorization has to happen on the server, at subscribe time, checked per channel, and re-checked when access changes or a client reconnects. The channel is the permission boundary, and the server, not the client, decides who gets to join.
Client-side filtering is a data leak with extra steps
Here is the anti-pattern. A client subscribes to a broad channel, receives everything on it, and hides the parts the user should not see. On screen it looks fine. Underneath, the server sent that user data they have no right to, and the only thing standing between them and it is frontend code they fully control. Open the network inspector and every "filtered out" message is right there. You did not filter anything; you delivered private data and asked the browser to please look away.
This is why pub/sub channel granularity is a security decision as much as a performance one. If a channel carries data spanning multiple permission levels, you have made it impossible to authorize cleanly, because there is no single yes-or-no answer to "can this user be on this channel." Design channels so that each one maps to exactly one authorization boundary, and the authorization question becomes answerable.
Authorize at subscribe time, on the server
The correct model is that subscribing is a privileged action the server must approve. When a client asks to join a channel, the server checks whether that user is allowed on that specific channel, using the same authorization logic that protects the rest of your app, and either grants or refuses the subscription. If refused, the client never receives a single message from that channel. The gate is at the door, not on the client's side of it.
This mirrors how you already protect a REST endpoint: you would never return private records and trust the client to hide them. A subscription is a long-lived read of a stream, so it deserves at least as much scrutiny as a one-time read. Run the check server-side, tied to the authenticated identity of the connection, and make the channel name meaningful enough that the check is a clean permission lookup.
Authentication of the connection itself is the prerequisite, and it has its own pitfalls I covered in securing WebSocket connections. Authenticate who the connection belongs to first, then authorize what that identity may subscribe to. Both steps, in that order, on the server.
Re-check on reconnect and on permission changes
Authorization is not a one-time event at subscribe. Two things can change after a client is subscribed: the connection can drop and reconnect, and the user's permissions can change. Both demand a fresh check.
On reconnect, do not blindly restore previous subscriptions. A reconnect is a new connection; re-authenticate it and re-authorize every channel it tries to rejoin. Otherwise a token that expired or access that was revoked during the disconnect could be quietly honored on the way back. Tie this into your reconnection handling so that resuming a session always runs the gate again rather than assuming the old grant still holds.
When permissions change mid-session, the server has to act on live subscriptions. If a user is removed from a project, their active subscription to that project's channel should be revoked then and there, not left running until they happen to reconnect. That means the authorization layer and the subscription layer have to talk to each other: a permission change is an event that can forcibly drop subscriptions that are no longer allowed.
Make it the platform's job to enforce
Subscribe-time authorization, tied to your app's permission model, re-checked on reconnect, and revocable mid-session, is a lot to build correctly on a raw socket server, and getting it wrong is a silent data leak rather than a loud crash. That is the worst kind of bug, because nothing tells you it happened. I want this enforced by the infrastructure with hooks into my own authorization logic, which is one reason I run real-time features on AltoHost: the platform gates subscriptions through my auth checks instead of leaving me to bolt security onto a stream after the fact. For the broader picture of dividing responsibility between app and platform, I wrote the backend real-time apps actually need.
The test is adversarial: try to subscribe to a channel you should not have access to, straight from a client, bypassing the UI. If you receive any data, your security lives on the client and you have a leak. If the server refuses the subscription outright, you built it right. Authorize the subscription, not the pixels. The channel is the boundary, and the server is the guard.