With love for developers of all backgrounds around the world ❤️
examples/socket is a small app built around
channels, spaces & handlers: four pages, each one a complete socket
feature in a single file. The buttons above and below open the full source.
The shared piece is one channel (src/lib/channel.tsx): the connector reads
a nickname cookie and returns it as the connection identity — no cookie, and the
connect is denied with preventRetry, so a signed-out client stops knocking
until the sign-in calls reconnectAll(). The channel is resumable: true, and
one <appChannel.Connection> at the app root (src/app.client.tsx) holds the
socket for the whole app (gate={false} — the sign-in header renders before any
connect; each page holds its own live section with gate instead, so it shows
the channel's loading until the connect settles, then the interface or the
deny). Everything else lives in the page that uses it — the space, its handlers,
and its server code sit next to the component that renders them, and the
compiler strips each side out of the other bundle:
pages/chat.tsx — topics with persisted messages (each topic is one room
of the space): a .joiner that admits by topic name (with a clean deny), a
serverHandler that writes to Prisma and pushes to the room, a buffering
clientHandler (resumable: true), and the one-condition catch-up —
onEnter: ({ gapless }) => gapless || refetch().pages/presence.tsx — who is in the room right now: the
presence recipe as written — the
pointSpaceJoinServerSuccess / pointSpaceLeaveServer events compute the
roster once and push it, a .query()-flavored handler serves the first paint,
and the push lands via setSocketQueryData with zero refetches.pages/notifications.tsx — a personal room per user: an enroller-only
space (no .joiner — nobody joins from the browser), pushed to from a plain
HTTP mutation.pages/board.tsx — one shared, DB-less room: a global space
(joiner(() => ({}))) and a bare space-wide push fanning every click out.The engine side is one option — server: { socket: true } in src/engine.ts —
which also turns the socket feature on for the client bundle. There is no
backplane configured: a single process needs none (add
backplane: process.env.REDIS_URL when you run several — see
the backplane).
Sign-in is deliberately primitive — a nickname cookie, no passwords
(src/lib/auth.tsx): the point is what happens at connect time, not the auth
stack. The header's sign-in / sign-out buttons call reconnectAll() /
disconnectAll() — the client half of a login flow (src/layouts/general.tsx).
Run it like basic:
cd examples/socket
bun install
bun run setup
bun run devOpen two browser windows side by side to see the pushes cross. The tests/
folder drives the same scenarios in a real browser with Playwright —
chat.e2e.test.ts (a cross-tab room push, the resume streams replaying a blip,
a reload showing the persisted history) and presence.e2e.test.ts (the roster
reacting to joins and leaves):
bun run test