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.

  1. Log in to Upstash and create a Redis database.

  2. Add the following env vars to .dev.vars:

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
  1. Run your opennext locally with:
pnpm cf:preview
  1. Point your client’s baseUrl to the Worker backend on port 8080:
import type { AppRouter } from "@/resources/index"
import { createClient } from "shapeless"
 
export const client = createClient<AppRouter>({
  baseUrl: "http://localhost:8080/api",
})

Deployment

  1. Deploy to Cloudflare Workers:
pnpm cf:deploy
  1. Set your Redis env vars in Workers:
wrangler secret put UPSTASH_REDIS_REST_URL
wrangler secret put UPSTASH_REDIS_REST_TOKEN
  1. 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.