WebSocket events are defined in src/sockets/<domain>/index.ts using SocketRouter. They're separate from HTTP routes but follow the same domain-based organization.
Use createSocketRouter() to declare emitters (server pushes to client) and listeners (client sends to server):
import { createSocketRouter } from "@backend/libs/websocket/socket-router";
import { wsValidate } from "@backend/libs/websocket/ws-validate";
import { z } from "zod";
const markReadSchema = z.object({ notificationId: z.string() });
export const notificationSockets = createSocketRouter()
.emitter("new")<WsNotificationPayload>()
.emitter("count")<WsUnreadCountPayload>()
.listener("mark-read", wsValidate(markReadSchema), async (c) => {
await markReadMutation({
notificationId: c.data.notificationId,
userId: c.userId,
});
});
wsEmit in your feature code.wsValidate(schema) for Zod validation.Mount socket routers in src/sockets/index.ts:
const sockets = new SocketRouter({ authenticate: wsAuthenticate })
.socket("/notification", notificationSockets)
.socket("/invitation", invitationSockets);
export const registerSockets = (app: Hono) => {
app.route("/ws", sockets.routes());
};
Use wsEmit from @backend/sockets/emit for type-safe server-side pushes:
import { wsEmit } from "@backend/sockets/emit";
wsEmit.toUser({ userId, topic: "notification/new", data: notification });
wsEmit.toUsers({ userIds, topic: "notification/new", data: notification });
wsEmit.toOrganization({ organizationId, topic: "invitation/new", data: invitation });
wsEmit.broadcast({ topic: "notification/count", data: { unreadCount: 0 } });
Topics and payloads are type-checked at compile time. A typo in the topic or wrong payload shape is a type error.
Socket event types are exported from the backend and picked up by the frontend's socketSdk automatically. No manual type wiring needed. See Frontend WebSockets.
src/sockets/<domain>/index.ts with createSocketRouter()src/sockets/index.ts with .socket("/prefix", router)wsEmit in your feature code to push events