Websockets with TanStack Start
For the past few weeks I’ve been playing around with TanStack Start, a new full-stack React framework by Tanner Linsley, the creator of React Query and other amazing TanStack libraries. The framework uses Nitro under the hood - the same server technology that powers Nuxt and SolidStart.
SSR, Streaming, Server Functions, API Routes, bundling and more powered by TanStack Router, Nitro, and Vite. Ready to deploy to your favorite hosting provider.
Nitro WebSockets
Traditionally, setting up WebSockets in a full-stack metaframework requires you to use a separate service or a custom server. Nitro changes this by providing experimental WebSocket support, powered by h3 and crossws, allowing our full-stack apps to handle WebSocket connections directly with minimal setup.
You can read more at nitro.build/guide/websocket.
Setting up the server
Assuming you already have a TanStack Start project set up (or you can use my tanstarter template), let’s get started by setting up a new websocket handler.
- First, let’s enable experimental Nitro websockets in our app config.
export default defineConfig({ // ... server: { experimental: { websocket: true, }, },});
- Next, let’s create a new websocket handler in our app.
import { defineEventHandler, defineWebSocket } from "@tanstack/start/server";
export default defineEventHandler({ handler() {}, websocket: defineWebSocket({ open(peer) { peer.publish("test", `User ${peer} has connected!`); peer.send("You have connected successfully!"); peer.subscribe("test"); }, async message(peer, msg) { const message = msg.text(); console.log("msg", peer.id, message); peer.publish("test", message); peer.send("Hello to you!"); }, async close(peer, details) { peer.publish("test", `User ${peer} has disconnected!`); console.log("close", peer.id, details.reason); }, async error(peer, error) { console.log("error", peer.id, error); }, }),});
- Finally, add a new router to handle the websocket requests.
export default defineConfig({ // ... server: { experimental: { websocket: true, }, },}).addRouter({ name: "websocket", type: "http", handler: "./app/ws.ts", // the file we created above target: "server", base: "/_ws",});
Our server is now ready to handle websocket connections at /_ws
. We can test it out by connecting from our client.
Example client
Here’s a minimal example implementation of a websocket client connecting to our server. You probably want to use a library or a more robust implementation in a real-world scenario.
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => { const ws = new WebSocket("ws://localhost:3000/_ws"); ws.onmessage = (event) => { console.log("Received message:", event.data); setMessages((prevMessages) => [...prevMessages, event.data]); }; ws.onerror = (error) => { console.error("WebSocket Error:", error); };
return () => { ws.close(); };}, []);
return ( <div> <h1>Websocket messages</h1> <ul> {messages.map((message, index) => ( <li key={index}>{message}</li> ))} </ul> </div>);
Handling authentication
You can perform authentication checks in the upgrade
hook. This is supported in crossws 0.3.0 and later.
export default defineEventHandler({ handler() {}, websocket: defineWebSocket({ async upgrade(req) { const user = await yourOwnAuthMethod(req); // e.g. check jwt
// deny unauthorized connections if (!user) { return new Response(null, { status: 401 }); } }, open(peer) { // ... }, }),});
crossws 0.3.2 and later allows you to add context in the upgrade
hook, which you can access in the peer
object in other hooks to preserve session data.
export default defineEventHandler({ handler() {}, websocket: defineWebSocket({ async upgrade(req) { const user = await yourOwnAuthMethod(req); // e.g. check jwt
// deny unauthorized connections if (!user) { return new Response(null, { status: 401 }); }
// auth successful req.context.user = { id: user.id, name: user.name, } }, open(peer) { console.log(peer.context.user); // { id: 1, name: 'Nate' } // ... }, }),});
Read more about crossws
hooks at crossws.unjs.io/guide/hooks.
This opens up a lot of possibilities for building real-time applications with modern full-stack frameworks. For example, SvelteKit has an ongoing PR to add native WebSocket support, also powered by crossws.
In the meantime, I’m also building a new project that uses TanStack Start and Nitro websockets, which I’ll share more about soon. Stay tuned!