Realtime Chat Architecture with Socket.IO
Follow one message from authorization to storage, delivery, acknowledgement, and reconnect recovery.
Connection is not membership
The socket handshake authenticates a user. It does not prove that the user may join every room they name. On each join, the server loads membership or evaluates permission, then attaches the socket to the room. A client-supplied user ID is never the identity source.
Membership can change while the socket remains open. For sensitive rooms, revocation needs a path that removes active sockets or rechecks authorization before write operations.
A message crosses five boundaries
The client creates a temporary ID and sends the room ID, content, and that ID. The server validates membership and payload, persists a message with a server sequence, then emits the canonical record. The acknowledgement maps the temporary ID to the stored ID so the sender can replace its pending item instead of rendering a duplicate.
Persist-before-broadcast gives history a clear source of truth. It adds a database write to the visible path, but recipients never see a message that disappears on refresh. For a chat product, that tradeoff is usually worth it.
A uniqueness constraint on sender and client message ID makes a network retry return the existing record. It also covers the case where the server saved the message but the acknowledgement never reached the client.
Scaling changes delivery, not ownership
With several Socket.IO instances, a Redis adapter forwards room events so users connected to different nodes receive the same broadcast. The database remains the owner of message history. Redis is the fanout path, not the record of what was said.
Presence is softer state. Heartbeats, disconnect timers, and multi-device sessions mean online is an estimate. Last seen and device-aware connection counts are often more honest than a single boolean.
Files take another route
I do not send large file bodies through the socket. The client uploads to object storage through an authorized endpoint or signed URL, the server validates the resulting metadata, and the chat message carries a reference to the attachment.
The authorization check still applies when the file is downloaded. An unguessable URL is not access control, and deleting a message may require a separate retention decision for the stored object.
Reconnect from a cursor
After reconnect, the client sends the last confirmed sequence for each active room. The server returns later messages in order, and the client merges them by canonical ID. This covers events missed during the gap without refetching an entire conversation.
The UI keeps pending, sent, delivered, and failed distinct. A timeout does not necessarily mean the message failed; it means the client does not know. Retrying with the same client message ID lets the server resolve that uncertainty safely.
Realtime feels reliable when disconnection is treated as an ordinary state. The live socket is only the fast path; persistence and cursor recovery are what make the conversation durable.