WebSocket procedures enable real-time bidirectional communication between the client and server without the need to manage any kind of infrastructure 🥳.
Important: JSandy's WebSocket implementation is designed specifically for Cloudflare Workers. This is because Cloudflare Workers allow long-lived real-time connections while Vercel and other Node.js runtime providers do not.
A WebSocket handler receives the following objects:
c: Hono context, e.g. headers, request info, env variables
ctx: Your context, e.g. database instance, authenticated user
io: Connection manager for sending messages to clients
Example: In the WebSocket router below, we implement a basic chat:
Validate incoming/outgoing messages using the chatValidator
Manage WebSocket connections and room-based message broadcasting
server/routers/chat-router.ts
import { z } from "zod"import { j } from "@jsandy/rpc"constchatValidator = z.object({ message: z.object({ roomId: z.string(), message: z.string(), author: z.string(), }),})export const chatRouter = j.router({ chat: j.procedure .incoming(chatValidator) .outgoing(chatValidator) .ws(({ c, io, ctx }) => ({ async onConnect({ socket }) { socket.on("message", async (message) => { // Optional: Implement message persistence // Example: await db.messages.create({ data: message }) // Broadcast the message to all clients in the room await io.to(message.roomId).emit("message", message) }) }, })),})
import { z } from "zod"import { j } from "@jsandy/rpc"constchatValidator = z.object({ message: z.object({ roomId: z.string(), message: z.string(), author: z.string(), }),})export const chatRouter = j.router({ chat: j.procedure .incoming(chatValidator) .outgoing(chatValidator) .ws(({ c, io, ctx }) => ({ async onConnect({ socket }) { socket.on("message", async (message) => { // Optional: Implement message persistence // Example: await db.messages.create({ data: message }) // Broadcast the message to all clients in the room await io.to(message.roomId).emit("message", message) }) }, })),})
You can now listen to (and emit) real-time events on the client:
app/page.tsx
"use client"import { client } from "@/lib/client"import { useWebSocket } from "jsandy/client"/** * Connect socket above component to avoid mixing * component & connection lifecycle */const socket = client.post.chat.$ws()export default function Page() { // 👇 Listening for incoming real-time events useWebSocket(socket, { message: ({ roomId, author, message }) => { console.log({ roomId, author, message }) }, }) return ( <button onClick={() => { // 👇 Send an event to the server socket.emit("message", { author: "John Doe", message: "Hello world", roomId: "general", }) }} > Emit Chat Message </button> )}
"use client"import { client } from "@/lib/client"import { useWebSocket } from "jsandy/client"/** * Connect socket above component to avoid mixing * component & connection lifecycle */const socket = client.post.chat.$ws()export default function Page() { // 👇 Listening for incoming real-time events useWebSocket(socket, { message: ({ roomId, author, message }) => { console.log({ roomId, author, message }) }, }) return ( <button onClick={() => { // 👇 Send an event to the server socket.emit("message", { author: "John Doe", message: "Hello world", roomId: "general", }) }} > Emit Chat Message </button> )}
WebSockets Setup
Development
To make scalable, serverless WebSockets possible, JSandy uses Upstash Redis as its real-time engine. Deploying real-world, production WebSocket applications is possible without a credit card, entirely on their free tier.
Side note: In the future, I'd like to add the ability to provide your own Redis connection string (e.g. self-hosted).
After logging into Upstash, create a Redis database by clicking the Create Database button
Copy the UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN env variables into a .dev.vars file in the root of your app
.dev.vars
UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN=
UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN=
Start your Cloudflare backend using
Terminal
wrangler dev
wrangler dev
Point the client baseUrl to the Cloudflare backend on port 8080:
import type { AppRouter } from "@/server"import { createClient } from "@jsandy/rpc"export const client = createClient<AppRouter>({ // 👇 Point to Cloudflare Worker API baseUrl: "http://localhost:8080/api",})
import type { AppRouter } from "@/server"import { createClient } from "@jsandy/rpc"export const client = createClient<AppRouter>({ // 👇 Point to Cloudflare Worker API baseUrl: "http://localhost:8080/api",})
That's it! 🎉 You can now use WebSockets for your local development. See below for an example usage.
Deployment
Deploy your backend to Cloudflare Workers using wrangler:
Terminal
wrangler deploy src/server/index.ts
wrangler deploy src/server/index.ts
Reason: Serverless functions, such as those provided by Vercel, Netlify, or other serverless platforms, have a maximum execution limit and do not support long-lived connections. Cloudflare workers do.
The console output looks like this:
Add the deployment URL to the client:
lib/client.ts
import type { AppRouter } from "@/server"import { createClient } from "@jsandy/rpc"export const client = createClient<AppRouter>({ baseUrl: `${getBaseUrl()}/api`,})function getBaseUrl() { // 👇 In production, use the production worker if (process.env.NODE_ENV === "production") { return "https://<YOUR_DEPLOYMENT>.workers.dev/api" } // 👇 Locally, use wrangler backend return `http://localhost:8080`}
import type { AppRouter } from "@/server"import { createClient } from "@jsandy/rpc"export const client = createClient<AppRouter>({ baseUrl: `${getBaseUrl()}/api`,})function getBaseUrl() { // 👇 In production, use the production worker if (process.env.NODE_ENV === "production") { return "https://<YOUR_DEPLOYMENT>.workers.dev/api" } // 👇 Locally, use wrangler backend return `http://localhost:8080`}
Set the UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN env variables in your Worker so it can access them:
Terminal
# Create UPSTASH_REDIS_REST_URL environment variable wrangler secret put UPSTASH_REDIS_REST_URL # Create UPSTASH_REDIS_REST_TOKEN environment variable wrangler secret put UPSTASH_REDIS_REST_TOKEN
# Create UPSTASH_REDIS_REST_URL environment variable wrangler secret put UPSTASH_REDIS_REST_URL # Create UPSTASH_REDIS_REST_TOKEN environment variable wrangler secret put UPSTASH_REDIS_REST_TOKEN
That's it! 🎉 If you now deploy your app to Vercel, Netlify, etc., the client will automatically connect to your production Cloudflare Worker.
You can verify the connection by sending a request to: