WebSockets

WebSocket procedures enable real-time bi-directional communication between the client and server without the need to manage any kind of infrastructure ๐Ÿฅณ.

Important: JStack'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
import { j } from "../jstack"
 
export const postRouter = j.router({
  chat: j.procedure.ws(({ c, io, ctx }) => ({
    async onConnect({ socket }) {
      // ...
    },
  })),
})

WebSockets Example

WebSockets are incredible for real-time features:

  • Collaborative editing
  • Real-time chat
  • Live dashboard updates

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 "jstack"
 
const chatValidator = 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 "jstack/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, JStack 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).

  1. After logging into Upstash, create a Redis database by clicking the Create Database button

    Create an Upstash Redis database
  2. Copy the UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN env variables into a .dev.vars file in the root of your app

    Copy Upstash Redis environment variables

    .dev.vars

     UPSTASH_REDIS_REST_URL=
     UPSTASH_REDIS_REST_TOKEN=
  3. Start your Cloudflare backend using

    Terminal

    wrangler dev
  4. Point the client baseUrl to the Cloudflare backend on port 8080:

    import type { AppRouter } from "@/server"
    import { createClient } from "jstack"
     
    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 examle usage.


Deployment

  1. Deploy your backend to Cloudflare Workers using wrangler:

    Terminal

    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:

    Deploy JStack WebSockets to Cloudflare
  2. Add the deployment URL to the client:

    lib/client.ts

    import type { AppRouter } from "@/server"
    import { createClient } from "jstack"
     
    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`
    }
    1. 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
    Use Wrangler to upload environment variables

    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:

     wss://<YOUR_DEPLOYMENT>.workers.dev/api/<ROUTER>/<PROCEDURE>
    Verify your JStack WebSocket connection