On this page
websockets
introduction
dframework ships with a fully integrated socket layer built on top of the ws library. it provides a dedicated event router that mirrors the http Route architecture, complete with middleware support, controller resolution, session authentication, and html rendering. the socket server boots automatically alongside your http server. there is nothing to install separately.
on the client side, the framework's frontend runtime exposes a global Socket facade that manages the connection, event emission, and event listening. the socket connection is established automatically when the spa router initializes.
for information on d-wire and d-live reactive elements that build on top of this socket layer, see the reactivity documentation.
how it works
when your application starts, the framework attaches a socket upgrade handler to the http server on the /ws path. when a browser opens a websocket connection, the framework creates a full Request object from the upgrade request, giving you access to cookies, sessions, headers, and the authenticated user inside your socket event handlers.
incoming messages are expected as json objects with an event property. the socket router looks up the registered handler for that event name and runs it through the middleware pipeline, exactly as an http route would be processed.
defining socket events
socket events are defined in your route files, typically routes/wire.js. you register events using the global Socket facade, which delegates to the underlying SocketRouter instance.
1import { Socket } from 'dframework';
event handlers
the Socket.on() method registers a handler for a named event. the event name uses colon separated namespacing by convention.
1Socket.on('chat:message', (req) => {2 const { text } = req.body;3 return json({ received: true, text });4});the handler receives the same arguments as an http route handler. req contains the full request object with access to req.body (the event payload), req.user, req.session, req.cookies, and all standard request methods. the global response helpers (render(), json(), redirect(), abort(), cookie()) work inside socket handlers exactly as they do in http controllers.
if the handler returns a value without calling any response method, the framework automatically wraps it in a json response with a 200 status code.
inline handlers
for simple events, you can use an inline function directly.
1Socket.on('ping', () => {2 return json({ pong: true, time: Date.now() });3});
controller handlers
for complex logic, reference a controller method using the familiar Controller@method string syntax. the framework resolves and imports the controller dynamically.
1Socket.on('chat:message', 'ChatController@onMessage');2Socket.on('chat:typing', 'ChatController@onTyping');1export default class ChatController {2 async onMessage(req) {3 const { text } = req.body;4 await Message.create({ user_id: req.user.id, text });5 Socket.broadcast('chat:new', { text, user: req.user.name });6 }7 8 async onTyping() {9 return json({ typing: true });10 }11}socket wire controllers are stored in the same controllers directory as http controllers and follow the same conventions. a single controller can handle both http routes and socket events.
middleware
socket events support the same middleware system as http routes. middleware functions receive req and next, and must call next() to proceed to the handler.
inline middleware
you can attach middleware to a single event by passing an array where the last element is the handler and all preceding elements are middleware references.
1Socket.on('admin:action', ['AuthMiddleware@requireAuth', 'AdminController@execute']);
grouped middleware
the Socket.group() method applies shared middleware to multiple events at once.
1Socket.group({ middleware: 'AuthMiddleware@requireAuth' }, (auth) => {2 auth.on('profile:update', 'ProfileController@update');3 auth.on('profile:avatar', 'ProfileController@avatar');4});you can also chain middleware using the Socket.middleware() method for a more fluent api.
1Socket.middleware('AuthMiddleware@requireAuth').group({}, (auth) => {2 auth.on('dashboard:refresh', 'DashboardController@refresh');3});here is how socket events look in a real application:
1import { Socket } from 'dframework';2 3Socket.on('pair:status', 'auth.AuthController@wire_status').targets(['#qr-box']);4 5Socket.group({ middleware: ['AuthMiddleware@requireAuth'] }, (auth) => {6 auth.on('follow:toggle', 'app.FriendsController@toggle').targets(['#leaderboard-list', '#friends-list-container']);7 auth.on('friends:tab', 'app.FriendsController@friends_tab').targets(['#friends-list-container']);8 auth.on('playback:heartbeat', 'app.PlaybackController@handleHeartbeat');9});
responding to events
socket handlers use the same global response helpers available in http controllers. all responses are delivered over the socket connection.
rendering html
the render() helper compiles a view template and sends the resulting html to the client. the framework morphs the html into the specified target element on the client side without a page refresh.
1Socket.on('feed:refresh', async () => {2 const posts = await Post.latest(20);3 return render('partials.feed', { posts });4});the rendered html is sent as a d-wire:render event. the client side runtime parses it and morphs the target dom element, preserving focus state, input values, and scroll positions.
the framework uses payload hashing to prevent redundant dom updates. if the rendered html produces the same hash as the previous render for that target, the update is silently skipped.
sending json
the json() helper sends a json payload back to the client.
1Socket.on('stats:fetch', async () => {2 const count = await User.count();3 return json({ users: count });4});
redirects
the redirect() helper sends a redirect instruction to the client. the browser will navigate to the specified url.
1Socket.on('session:expired', () => {2 return redirect('/login');3});
aborting
the abort() helper sends an error response with a status code and optional message.
1Socket.on('admin:action', (req) => {2 if (!req.user?.isAdmin) {3 return abort(403, 'forbidden');4 }5});the default status messages are 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests, and 500 Internal Error.
cookies
you can set cookies from a socket handler using the cookie() helper. the cookies are sent alongside the response and the client side runtime applies them to the browser.
1Socket.on('theme:set', (req) => {2 cookie('theme', req.body.theme, { path: '/', sameSite: 'Lax' });3 return json({ ok: true });4});
target restrictions
you can restrict which dom target selectors an event is allowed to render into using the targets() chain method. if the client sends a target that is not in the allowed list, the event is silently rejected.
1Socket.on('sidebar:update', 'SidebarController@render').targets('#sidebar');2Socket.on('panel:refresh', 'PanelController@render').targets(['#left-panel', '#right-panel']);
broadcasting
the Socket.broadcast() method sends an event to all connected websocket clients. you can optionally exclude a specific connection (typically the sender).
1Socket.on('chat:send', (req, res, ws) => {2 const message = { text: req.body.text, user: req.user.name };3 Socket.broadcast('chat:new', message, ws);4 return json({ sent: true });5});the third argument is the websocket connection to exclude from the broadcast. passing the current ws prevents the sender from receiving their own message.
the socket facade
the Socket global is a proxy facade that delegates all method calls to the application's SocketRouter instance. it is available globally after the application initializes and can be imported from the framework.
1import { Socket } from 'dframework';the facade exposes all SocketRouter methods: on(), group(), middleware(), broadcast(), and liveNotify().
client side usage
the framework's frontend runtime provides a global Socket object on the window that manages the websocket connection.
connecting
the websocket connection is established automatically when the spa router initializes. you can also connect manually.
1await Socket.connect();the connection url defaults to /ws and automatically uses wss: on secure pages and ws: on insecure ones. the full uri is constructed from the current page's location.host.
emitting events
use Socket.emit() to send an event to the server. you can pass a data payload and an optional target selector.
1Socket.emit('chat:send', { text: 'hello world' });1Socket.emit('dashboard:refresh', { filter: 'today' }, '#dashboard');when a target is specified, the server handler's rendered output will be morphed into that specific dom element.
listening for events
use Socket.on() on the client side to listen for events pushed from the server.
1Socket.on('chat:new', (data) => {2 appendMessage(data.text, data.user);3});for broadcast events, the callback receives the data payload directly.
origin protection
the socket server validates the Origin header of every upgrade request against your configured APP_URL. connections from mismatched origins are immediately destroyed to prevent cross-site websocket hijacking (cswsh) attacks.
in the local environment, connections from localhost and 127.0.0.1 are allowed regardless of the configured url.
debug mode
in the local environment with debug mode enabled, the socket router attaches detailed timing and query information to every response. the debugbar on the client side displays the elapsed time, executed queries, and request headers for each socket event, providing the same level of visibility as http requests.

