Back to index

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 Vinxi under the hood, which uses Nitro — the same server technology that powers Nuxt and SolidStart. 1

Full-document SSR, Streaming, Server Functions, bundling and more, powered by TanStack Router, Vinxi, and Vite. Ready to deploy to your favorite hosting provider.

tanstack.com/start

Nitro WebSockets

Nitro provides experimental support for WebSockets, powered by h3 and crossws. This feature can be utilized in Vinxi to enable websockets in our TanStack Start projects.

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.

  1. First, let’s enable experimental Nitro websockets in our app config.
app.config.ts
export default defineConfig({
// ...
server: {
experimental: {
websocket: true,
},
},
});
  1. Next, let’s create a new websocket handler in our app.
app/ws.ts
import { defineEventHandler, defineWebSocket } from "vinxi/http";
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);
},
}),
});
  1. Finally, add a new Vinxi router to handle the websocket requests.
app.config.ts
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[]>([]);
const [socket, setSocket] = useState<WebSocket | null>(null);
useEffect(() => {
const ws = new WebSocket("ws://localhost:3000/_ws");
setSocket(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

Ideally, you’d want to handle authentication on the websocket handler’s upgrade hook.

app/ws.ts
export default defineEventHandler({
handler() {},
websocket: defineWebSocket({
async upgrade(req) {
const isAuthorized = await yourOwnAuthMethod(req);
// deny unauthorized connections
if (!isAuthorized) {
// only works in crossws 0.3+
return new Response("Unauthorized", { status: 401 });
}
},
open(peer) {
// ...
},
}),
});

This is supported in crossws 0.3+ or Nitro 2.10+, but is currently not available in Vinxi. I will be updating this post with a full working example repo once Vinxi supports this feature.


Footnotes

  1. SolidStart is a full-stack framework for Solid that also uses Vinxi. This guide should likely work for SolidStart as well, with some adjustments.