LumenJS deep dive — Socket

This article is part of the LumenJS deep dive series a series about the architectural decisions behind LumenJS, an open-source full-stack web framework designed for agent-driven development.

subscribe() solved one direction. Server pushes, client listens. But some features need both sides talking. Chat, collaborative editing, presence, games. For that we added socket().

Socket

Same pattern, different primitive. Each page can export a socket() function. It runs on the server, sets up event listeners, and gives you a two-way channel — the client can send, the server can respond or broadcast.

Code sample from socket

The server listens with on(). It pushes back with push(). It broadcasts to a room with room.broadcast(). The cleanup function runs when the client disconnects.

On the component side, the framework injects an emit() method automatically, no wiring, no import.

Code sample from socket

That's the full loop. Client emits, server receives, server broadcasts, components update.

Code sample from socket

Rooms

Rooms are first-class. Join on connect, leave on disconnect, broadcast to everyone or everyone except the sender.

Code sample from socket

broadcast() excludes the sender. broadcastAll() includes everyone. Pick what fits.

When to use subscribe vs socket

The decision is simple. If the data only flows one way, server to client, use subscribe(). It uses native SSE, zero dependencies. If the client needs to send events too, use socket(). It uses Socket.IO, installed separately with lumenjs add socketio.

No overhead either way. Socket.IO client is only loaded on pages that export a socket() function.

When socket grows

Same rule as loaders and subscribe. Move the handler out to _socket.ts in the page folder. LumenJS picks it up automatically.

Code sample from socket

Conclusion

Three primitives. loader() for initial data. subscribe() for server push. socket() for bidirectional communication. Each one follows the same convention, a named export, collocated with the component, stripped from the client bundle.

The tradeoff here is infrastructure. Socket.IO needs a stateful server with sticky sessions. Traditional serverless functions don't fit. Plan for it before you reach for it.

Next in LumenJS deep dive LumenJS deep dive — llms.txt