WebSockets
WebSockets
WebSocket procedures enable real-time two-way communication between client and server with zero infrastructure management 🥳.
Important: Shapeless WebSockets are built for Cloudflare Workers only, because Workers support long-lived real-time connections. Node.js platforms like Vercel do not support this.
A WebSocket handler receives:
c
: Hono context (headers, request info, env vars)ctx
: Your app context (DB, auth info, etc.)io
: Connection manager for broadcasting messages
import { app } from "../shapeless"
export const postRouter = app.router({
chat: app.procedure.ws(({ c, io, ctx }) => ({
async onConnect({ socket }) {
// ...
},
})),
})
WebSockets Example
WebSockets are great for things like:
- Collaborative editing
- Real-time chat
- Live dashboards
Here’s a simple chat example that validates messages and broadcasts them to all clients in a room:
// resources/routers/chat-router.ts
import { z } from "zod"
import { app } from "../shapeless"
const chatValidator = z.object({
message: z.object({
roomId: z.string(),
message: z.string(),
author: z.string(),
}),
})
export const chatRouter = app.router({
chat: app.procedure
.incoming(chatValidator)
.outgoing(chatValidator)
.ws(({ c, io }) => ({
async onConnect({ socket }) {
socket.on("message", async (message) => {
// Optionally save message in DB here
// Broadcast to all clients in the room
await io.to(message.roomId).emit("message", message)
})
},
})),
})
On the client, you can listen for and send real-time events like this:
"use client"
import { client } from "@/shapeless.client.ts"
import { useWebSocket } from "shapeless/client"
const socket = client.post.chat.$ws()
export default function Page() {
useWebSocket(socket, {
message: ({ roomId, author, message }) => {
console.log({ roomId, author, message })
},
})
return (
<button
onClick={() =>
socket.emit("message", {
author: "John Doe",
message: "Hello world",
roomId: "general",
})
}
>
Send Chat Message
</button>
)
}
Setup
Development
Shapeless uses Upstash Redis as the real-time backend for WebSockets, allowing production-ready WebSockets without any billing.
-
Log in to Upstash and create a Redis database.
-
Add the following env vars to
.dev.vars
:
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
- Run your opennext locally with:
pnpm cf:preview
- Point your client’s
baseUrl
to the Worker backend on port8080
:
import type { AppRouter } from "@/resources/index"
import { createClient } from "shapeless"
export const client = createClient<AppRouter>({
baseUrl: "http://localhost:8080/api",
})
Deployment
- Deploy to Cloudflare Workers:
pnpm cf:deploy
- Set your Redis env vars in Workers:
wrangler secret put UPSTASH_REDIS_REST_URL
wrangler secret put UPSTASH_REDIS_REST_TOKEN
- Update client to use production URL:
function getBaseUrl() {
if (process.env.NODE_ENV === "production") {
return "https://<YOUR_WORKER>.workers.dev/api"
}
return "http://localhost:8080"
}
export const client = createClient<AppRouter>({
baseUrl: getBaseUrl(),
})
Now your WebSocket client will connect to your Cloudflare Worker backend both locally and in production, with zero infrastructure hassle.