This is the full developer documentation for Zocket # CLI > Reference for the zocket command-line tool — auth, init, link, and deploy. The `zocket` CLI signs you in to the platform, creates or links projects, and deploys actor bundles. ## Install [Section titled “Install”](#install) The CLI ships as `@zocket/cli`. Install it globally, or use it via your package manager’s runner. ```bash bun add -g @zocket/cli # or npm i -g @zocket/cli ``` After install the `zocket` binary is on your path. ## Configuration [Section titled “Configuration”](#configuration) The CLI stores state in two JSON files. | File | Path | Contents | | ------------- | ----------------------- | ------------------------------------------------------------------- | | Global config | `~/.zocket/config.json` | `platformUrl`, `cliToken` (from `zocket auth`) | | Project link | `./.zocket.json` (cwd) | `projectId`, `projectSlug`, `projectDomain`, `deployToken`, `entry` | **Environment variables:** * `ZOCKET_PLATFORM_URL` — default control-plane URL. Falls back to `http://localhost:3000`. Every command accepts `--platform-url` to override per-invocation. ## Commands [Section titled “Commands”](#commands) ### `zocket auth` [Section titled “zocket auth”](#zocket-auth) Sign in through the platform device flow. ```bash zocket auth [--platform-url ] ``` The CLI hits `/api/auth/device/start`, prints a verification URL and user code, opens your browser if possible, and polls until approval. On success it writes `cliToken` to `~/.zocket/config.json` so later commands can authenticate. Run `zocket auth` again any time to refresh the token. ### `zocket init` [Section titled “zocket init”](#zocket-init) Create a new project on the platform and link the current directory to it. ```bash zocket init --name [--slug ] [--entry ] [--platform-url ] ``` | Flag | Required | Default | Description | | ---------------- | -------- | ---------------------- | --------------------------------- | | `--name` | ✓ | — | Human-readable project name | | `--slug` | | auto | Optional URL slug for the project | | `--entry` | | `index.ts` | Path to the actor entry file | | `--platform-url` | | `$ZOCKET_PLATFORM_URL` | Override the control plane | Requires `cliToken` (run `zocket auth` first). Writes `.zocket.json` in the current directory: ```json { "projectId": "prj_...", "projectSlug": "my-app", "projectDomain": "my-app.zocket.app", "deployToken": "dt_...", "entry": "index.ts" } ``` ### `zocket link` [Section titled “zocket link”](#zocket-link) Link the current directory to an **existing** project you already have access to. ```bash zocket link --project [--entry ] [--platform-url ] ``` | Flag | Required | Default | Description | | ---------------- | -------- | ---------------------- | ---------------------------- | | `--project` | ✓ | — | Existing project slug | | `--entry` | | `index.ts` | Path to the actor entry file | | `--platform-url` | | `$ZOCKET_PLATFORM_URL` | Override the control plane | Fetches the project, mints a fresh deploy token, and writes `.zocket.json` (same shape as `init`). ### `zocket deploy` [Section titled “zocket deploy”](#zocket-deploy) Bundle the linked project and push it to the platform. ```bash zocket deploy [--entry ] [--platform-url ] ``` | Flag | Required | Default | Description | | ---------------- | -------- | ------------------------------------------ | -------------------------- | | `--entry` | | value from `.zocket.json`, else `index.ts` | Entry file to bundle | | `--platform-url` | | `$ZOCKET_PLATFORM_URL` | Override the control plane | `zocket deploy` must be run from a linked directory (one containing `.zocket.json`). It bundles the entry file using Bun (`target: "bun"`, `format: "esm"`), uploads the bundle to `/api/deployments` with the project’s deploy token, and prints the new version, status, domain, and WebSocket URL. Under the hood the platform stores the bundle in S3, records a row in the `deployments` table, and triggers the runtime to fetch and load it. See the [Deployment overview](/deployment/overview/) for what happens after upload. ## Typical workflow [Section titled “Typical workflow”](#typical-workflow) ```bash # one-time zocket auth # in a new project zocket init --name "Realtime Todos" --entry src/server.ts # iterate bun run build # optional zocket deploy # join an existing project from another machine zocket link --project realtime-todos zocket deploy ``` # Actor Handles > Proxy methods, events, state subscriptions, and lifecycle management. An **Actor Handle** is the client-side proxy for a single actor instance. It provides typed methods, event subscriptions, state subscriptions, and lifecycle management. ## Getting a Handle [Section titled “Getting a Handle”](#getting-a-handle) ```ts const room = client.chat("room-1"); ``` This returns a typed `ActorHandle` — all methods, events, and state types are inferred. ## Calling Methods (RPC) [Section titled “Calling Methods (RPC)”](#calling-methods-rpc) Methods are accessed directly on the handle. Every method call is an RPC — a request sent over WebSocket with a unique ID, resolved when the server responds: ```ts // Method with input await room.sendMessage({ text: "hello" }); // Method without input const count = await counter.increment(); // Method with return value const { playerId, color } = await game.join({ name: "Alice" }); ``` ### How RPC works under the hood [Section titled “How RPC works under the hood”](#how-rpc-works-under-the-hood) 1. The client generates a unique request ID (`rpc_{counter}_{timestamp}`) 2. Sends `{ type: "rpc", id, actor, actorId, method, input }` over WebSocket 3. The server processes the method (queued, single-writer) and sends back `{ type: "rpc:result", id, result }` or `{ type: "rpc:result", id, error }` 4. The client matches the response by ID and resolves or rejects the promise ### Waiting for connection [Section titled “Waiting for connection”](#waiting-for-connection) RPCs automatically wait for the WebSocket to be ready. If the socket is reconnecting, the RPC waits (up to `rpcTimeout`) for it to reopen before sending: ```ts // This works even during a brief reconnect — the call waits for the socket const messages = await room.getMessages(); ``` The `rpcTimeout` covers the full lifecycle: wait-for-open + server response time. ### Error cases [Section titled “Error cases”](#error-cases) ```ts try { await room.sendMessage({ text: "hello" }); } catch (err) { // Possible errors: // - 'RPC "sendMessage" timed out after 10000ms' (timeout) // - 'WebSocket closed' (socket dropped while RPC was pending) // - 'Validation failed for sendMessage: [...]' (input validation) // - 'Forbidden' (middleware rejection) // - 'Unknown method: foo' (method doesn't exist) } ``` ### Return values [Section titled “Return values”](#return-values) If a handler returns a value, the client receives it. If a handler returns void, the client receives `undefined`. Both cases go through the same RPC flow — every method call gets an acknowledgement from the server: ```ts // Server: returns { count: number } const result = await counter.increment(); // { count: 1 } // Server: returns void await room.clear(); // undefined (but still waits for server confirmation) ``` This means you always know whether the server successfully processed your call, even for void methods. Errors (auth failures, validation, handler throws) are always surfaced. ## Event Subscriptions [Section titled “Event Subscriptions”](#event-subscriptions) Use `.on()` to listen for typed events: ```ts const unsubscribe = room.on("newMessage", (payload) => { // payload is typed: { text: string; from: string } console.log(`${payload.from}: ${payload.text}`); }); // Later: unsubscribe(); ``` Event subscriptions are **lazy** — the client only sends an `event:sub` message to the server when the first listener is added. When all listeners are removed, it sends `event:unsub`. After a reconnect, active event subscriptions are automatically re-sent to the server. ## State Subscriptions [Section titled “State Subscriptions”](#state-subscriptions) The `.state` object provides `subscribe()` and `getSnapshot()`: ```ts // Subscribe to state changes const unsub = room.state.subscribe((state) => { console.log("Current messages:", state.messages); }); // Read current state synchronously const current = room.state.getSnapshot(); ``` Like events, state subscriptions are lazy. The first `subscribe()` sends `state:sub` to the server, which triggers an immediate `state:snapshot` response. Subsequent mutations on the server are received as `state:patch` messages. After a reconnect, active state subscriptions are automatically re-sent, and the server sends a fresh snapshot. ### How State Updates Work [Section titled “How State Updates Work”](#how-state-updates-work) 1. Server mutates state via Immer → generates JSON patches 2. Server sends `state:patch` to all state subscribers 3. Client’s `StateStore` applies patches to local state copy 4. All registered listeners are notified ## Handle Metadata [Section titled “Handle Metadata”](#handle-metadata) The `meta` object provides the handle’s identity and lifecycle control: ```ts room.meta.name // "chat" room.meta.id // "room-1" room.meta.dispose() // decrement ref count ``` | Property | Type | Description | | :--------------- | :----------- | :-------------------------------- | | `meta.name` | `string` | The actor name (e.g. `"chat"`) | | `meta.id` | `string` | The instance ID (e.g. `"room-1"`) | | `meta.dispose()` | `() => void` | Decrement ref count | ## Disposing Handles [Section titled “Disposing Handles”](#disposing-handles) Call `meta.dispose()` when you’re done with a handle: ```ts room.meta.dispose(); ``` This decrements the reference count. When the ref count hits zero (after a one-tick delay), the handle: 1. Unsubscribes from events on the server 2. Unsubscribes from state on the server 3. Clears all listeners and state ### Reference Counting [Section titled “Reference Counting”](#reference-counting) Multiple consumers can share the same handle. Each call to `client.chat("room-1")` increments the ref count. Each `meta.dispose()` decrements it. The handle is only truly disposed when the count reaches zero. The one-tick delay on final disposal ensures React StrictMode’s temporary unmount/remount cycle doesn’t kill shared handles. # Creating a Client > Connect to your Zocket server with full type safety. The `@zocket/client` package provides `createClient` — a fully typed WebSocket client that infers its API from your app definition. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import { createClient } from "@zocket/client"; import type { app } from "./server"; const client = createClient({ url: "ws://localhost:3000", }); ``` The generic `` is the only type annotation you need. Everything else — actor names, method signatures, event payloads, state shapes — is inferred. ## Options [Section titled “Options”](#options) ```ts interface ClientOptions { /** WebSocket URL for the Zocket server. */ url: string; /** * Total timeout in ms for an RPC, including time spent waiting for a live * socket before the request can be sent. 0 = no timeout. Default: 10000. */ rpcTimeout?: number; /** Reject `$ready` if the initial connection is not established in time. Default: 10000. */ connectTimeout?: number; /** Automatically reconnect after unexpected socket closes. Default: true. */ reconnect?: boolean; /** Override reconnect delay in ms. Useful for deterministic tests. */ reconnectDelayMs?: number; } ``` ### RPC Timeout [Section titled “RPC Timeout”](#rpc-timeout) The `rpcTimeout` covers the **entire lifecycle** of an RPC call — from the moment you call the method to when the server responds. This includes time spent waiting for the WebSocket to be ready (e.g., during reconnection): ```ts const client = createClient({ url: "ws://localhost:3000", rpcTimeout: 5000, // 5 seconds total }); ``` If the socket is reconnecting and takes 2 seconds to open, the RPC has 3 seconds remaining for the server to respond. If the full timeout elapses, the promise rejects: ```ts try { await room.sendMessage({ text: "hello" }); } catch (err) { // 'RPC "sendMessage" timed out after 5000ms' } ``` Set to `0` to disable timeouts entirely. ### Connect Timeout [Section titled “Connect Timeout”](#connect-timeout) Rejects `connection.ready` if the initial connection isn’t established in time: ```ts const client = createClient({ url: "ws://localhost:3000", connectTimeout: 3000, }); try { await client.connection.ready; } catch (err) { // 'WebSocket did not connect within 3000ms' } ``` ## Getting Actor Handles [Section titled “Getting Actor Handles”](#getting-actor-handles) Access actors by name, then pass an instance ID: ```ts const room = client.chat("general"); const game = client.game("match-42"); ``` Each call returns a typed `ActorHandle` — see [Actor Handles](/client/actor-handles/) for the full API. ## Connection Lifecycle [Section titled “Connection Lifecycle”](#connection-lifecycle) ### `connection.ready` [Section titled “connection.ready”](#connectionready) A promise that resolves when the WebSocket connection is open: ```ts await client.connection.ready; console.log("Connected!"); ``` Also available as `client.$ready` for convenience. ### `client.connection` [Section titled “client.connection”](#clientconnection) The `connection` object provides everything you need for connection lifecycle management: ```ts // Wait for the connection to be ready await client.connection.ready; // Read current status synchronously client.connection.status // "connecting" | "connected" | "reconnecting" | "disconnected" // Subscribe to status changes const unsub = client.connection.subscribe((status) => { console.log("Connection:", status); }); // Close the connection client.connection.close(); ``` `connection.close()` (also available as `client.$close()`) gracefully shuts down the client: 1. Stops any pending reconnect attempts 2. Disposes all active actor handles (unsubscribes from events/state) 3. Rejects all pending RPCs with `"WebSocket client closed"` 4. Closes the WebSocket After closing, the client cannot be reused. Create a new client to reconnect. **Status transitions:** | From | To | When | | :------------- | :------------- | :------------------------------------------------------- | | `connecting` | `connected` | Initial WebSocket connection opens | | `connected` | `reconnecting` | Socket drops unexpectedly | | `reconnecting` | `connected` | Reconnect succeeds | | any | `disconnected` | `close()` called, or reconnect disabled and socket drops | **In React**, use the `useConnectionStatus()` hook instead (see [React Hooks](/react/hooks/)): ```tsx function StatusBanner() { const status = useConnectionStatus(); if (status === "reconnecting") return
Reconnecting...
; return null; } ``` ## Reconnection [Section titled “Reconnection”](#reconnection) By default, the client automatically reconnects when the WebSocket closes unexpectedly. Reconnection uses exponential backoff with jitter (250ms → 500ms → 1s → 2s cap). ```ts const client = createClient({ url: "ws://localhost:3000", reconnect: true, // default }); ``` When a reconnection succeeds: * All active event and state subscriptions are **automatically re-sent** to the server * State subscribers receive a fresh snapshot * New RPC calls work immediately When the socket drops during reconnection: * All **pending RPCs are rejected** — the caller gets an error and can decide whether to retry * The client schedules another reconnect attempt To disable reconnection: ```ts const client = createClient({ url: "ws://localhost:3000", reconnect: false, }); ``` ### Why pending RPCs are rejected (not resent) [Section titled “Why pending RPCs are rejected (not resent)”](#why-pending-rpcs-are-rejected-not-resent) Unlike some frameworks that buffer and resend pending requests on reconnect, Zocket rejects them. This is intentional — actor methods are often non-idempotent mutations (e.g., `sendMessage`). Auto-resending could cause duplicates if the server partially processed the request before the disconnect. The caller is in the best position to decide whether to retry. ## RPC Behavior [Section titled “RPC Behavior”](#rpc-behavior) All method calls on actor handles go through WebSocket as request/response messages: 1. Client generates a unique request ID and sends an `rpc` message 2. Server processes the method and sends back an `rpc:result` with the same ID 3. Client resolves the promise with the result (or rejects with the error) RPCs **wait for the socket to be ready** before sending. If the socket is currently reconnecting, the RPC waits (up to `rpcTimeout`) for it to reopen: ```ts // Works even if the socket is momentarily down — waits for reconnect const result = await room.getSnapshot(); ``` An RPC will reject if: * The `rpcTimeout` elapses (including wait time) * The WebSocket closes while the RPC is pending * The server returns an error (validation, middleware, handler throw) * `$close()` is called ## Error Handling [Section titled “Error Handling”](#error-handling) ```ts try { await room.sendMessage({ text: "hello" }); } catch (err) { console.error(err.message); // Possible messages: // - 'RPC "sendMessage" timed out after 10000ms' // - 'WebSocket closed' // - 'Validation failed for sendMessage: [...]' // - 'Forbidden' (from middleware) } ``` ## How It Works [Section titled “How It Works”](#how-it-works) `createClient` returns a `Proxy` object. When you access `client.chat`, it returns a function. Calling that function (`client.chat("room-1")`) creates or retrieves a shared `ActorHandleImpl` for that `(actorName, actorId)` pair. Handles are **reference-counted** — multiple consumers can share the same handle. When the ref count drops to zero, disposal is deferred by one tick to support React StrictMode’s unmount/remount cycle. # State Store > Client-side state management with snapshots and JSON patches. Each actor handle has an internal `StateStore` that manages the client-side copy of the actor’s state. ## How It Works [Section titled “How It Works”](#how-it-works) 1. When you call `handle.state.subscribe()`, the client sends a `state:sub` message 2. The server responds with a `state:snapshot` containing the full state 3. On each state change, the server sends `state:patch` with JSON Patch operations 4. The `StateStore` applies patches to its local copy and notifies subscribers ## StateStore API [Section titled “StateStore API”](#statestore-api) ```ts class StateStore { getState(): TState | undefined; setSnapshot(state: TState): void; applyPatches(patches: JsonPatchOp[]): void; subscribe(listener: (state: TState) => void): Unsubscribe; get subscriberCount(): number; } ``` The `StateStore` is internal to actor handles — you interact with it through the handle’s `.state` property, not directly. ## JSON Patch Support [Section titled “JSON Patch Support”](#json-patch-support) The store implements a subset of RFC 6902: | Operation | Description | | :-------- | :------------------------------------------------------ | | `add` | Add a value at a path (including array append with `-`) | | `replace` | Replace the value at a path | | `remove` | Remove the value at a path | ### Path Syntax [Section titled “Path Syntax”](#path-syntax) Paths follow JSON Pointer syntax (RFC 6901): ```plaintext /messages/0/text → state.messages[0].text /players/- → append to state.players array /count → state.count ``` Special characters are escaped: `~0` for `~`, `~1` for `/`. ## Immutability [Section titled “Immutability”](#immutability) Patches are applied using `structuredClone()` — each update produces a new state object. This ensures React’s `useSyncExternalStore` detects changes correctly. ## Subscriber Notification [Section titled “Subscriber Notification”](#subscriber-notification) Subscribers are called synchronously after each state update (snapshot or patch). If state is `undefined` (no snapshot received yet), subscribers are not called. When a new subscriber is added and state is already available, the subscriber is called immediately with the current state. ## Integration with React [Section titled “Integration with React”](#integration-with-react) The `useActorState` hook from `@zocket/react` is built on top of `StateStore`: ```tsx // Subscribes to the store, re-renders on changes const messages = useActorState(room, (s) => s.messages); ``` See [React Hooks](/react/hooks/) for details on selectors and memoization. # Comparison > How Zocket compares to Socket.io, PartyKit, Convex, Liveblocks, and others. Every tool in this space makes different tradeoffs. Here’s an honest look at when you’d pick each one, and where Zocket is meaningfully different. Zocket is strongest when your product is made of live entities that clients interact with directly: chats, agents, rooms, sessions, workflows, documents, or matches. If that is not the shape of your app, another tool may fit better. ## Socket.io [Section titled “Socket.io”](#socketio) The battle-tested standard for realtime in Node.js. **Strengths:** * Massive ecosystem and community — tons of tutorials, plugins, and production battle scars * Automatic fallback to HTTP long-polling when WebSockets aren’t available * Room and namespace abstractions built-in * Works with any language (has clients for Python, Java, Go, etc.) **Tradeoffs:** * Type safety is opt-in and manual — you define shared interfaces that can drift from the implementation * No runtime validation — payloads are untyped `any` by default * No built-in state management — you manage server state and sync it yourself * Event-string API (`socket.emit("event", ...)`) is error-prone at scale **Best for:** Teams that need broad runtime/language support, HTTP fallbacks, or are already invested in the Socket.io ecosystem. ## PartyKit (Cloudflare) [Section titled “PartyKit (Cloudflare)”](#partykit-cloudflare) Edge-first realtime platform built on Cloudflare Workers and Durable Objects. **Strengths:** * Runs at the edge — low latency globally without managing infrastructure * Each “party” is an isolated stateful server (similar concept to actors) * Hibernation support — parties sleep when idle, saving costs * Managed platform — no servers to operate, scales automatically * Good integration with Cloudflare’s ecosystem (KV, D1, R2) **Tradeoffs:** * Platform lock-in to Cloudflare — you can’t self-host or run on other clouds * Limited TypeScript inference across the wire — client types are manual * No automatic state sync — you send messages and manage client state yourself * Durable Object constraints (single-region per party, storage limits) * Cloudflare Workers runtime limitations (no Node.js APIs, execution time limits) **Best for:** Teams that want managed infrastructure at the edge and are comfortable with Cloudflare lock-in. Great for apps where latency matters more than type safety DX. ## Convex [Section titled “Convex”](#convex) Reactive backend-as-a-service with a built-in database. **Strengths:** * Full reactive backend — queries automatically re-run and push updates when underlying data changes * Built-in database with ACID transactions — no external DB to manage * Excellent TypeScript support — functions are typed end-to-end * Scheduled functions, cron jobs, file storage, and auth built-in * Handles caching, pagination, and optimistic updates out of the box **Tradeoffs:** * Fully managed platform — you can’t self-host or bring your own database * The reactive model is query-based, not actor-based — great for CRUD-like updates, less natural for game loops or stateful simulations * Vendor lock-in — your data and logic live on Convex’s infrastructure * Pricing scales with function calls and database usage * Not designed for raw WebSocket use cases like streaming binary data **Best for:** Apps that are primarily data-driven (dashboards, collaborative docs, social feeds) where you want a complete reactive backend without managing infrastructure. Excellent choice if you don’t need fine-grained control over the realtime protocol. ## Liveblocks [Section titled “Liveblocks”](#liveblocks) Collaborative infrastructure for building multiplayer experiences. **Strengths:** * Purpose-built for collaboration — presence, cursors, selection, comments, notifications out of the box * Conflict-free storage via Yjs/CRDT integration — true multi-writer without conflicts * Excellent React bindings with suspense support * Managed infrastructure — no backend to build for collaboration features * Rich pre-built components (comments, threads, notifications) **Tradeoffs:** * Collaboration-focused — not a general-purpose realtime framework * Managed service with per-MAU pricing — costs scale with users, not usage * Limited server-side logic — the server is Liveblocks’ infrastructure, not yours * You can’t define custom server-side methods or business logic in Liveblocks itself * Storage model is CRDT-based — powerful for collaboration, but overkill for simple state sync **Best for:** Teams building collaborative features (multiplayer cursors, shared editing, comments) who want polished, ready-made components. If your app is primarily about collaboration UX, Liveblocks gets you there faster than building from scratch. ## Supabase Realtime [Section titled “Supabase Realtime”](#supabase-realtime) Realtime layer on top of Postgres. **Strengths:** * Database-driven — changes to Postgres rows automatically broadcast to clients * Presence and broadcast channels built-in * Integrates with the full Supabase stack (auth, storage, edge functions) * Row-level security applies to realtime subscriptions * Open source — self-hostable **Tradeoffs:** * Tied to Postgres — realtime events are driven by database changes, not arbitrary server logic * No actor model or stateful server-side units * Limited control over message routing — it’s pub/sub over database changes * Not ideal for high-frequency updates (game state, cursor positions) due to database overhead * TypeScript support exists but isn’t as deeply inferred as purpose-built frameworks **Best for:** Apps already on Supabase that need realtime updates when database records change. Great for live feeds, notifications, and dashboards backed by Postgres. ## Ably / Pusher [Section titled “Ably / Pusher”](#ably--pusher) Managed pub/sub messaging platforms. **Strengths:** * Global edge infrastructure — low latency, high availability, no servers to manage * Protocol-level features: message ordering, delivery guarantees, history, presence * Broad SDK support — works with any language, any platform * Handles reconnection, deduplication, and exactly-once delivery **Tradeoffs:** * Pure message transport — no server-side logic, state management, or actors * You build the application layer yourself on top of pub/sub primitives * Per-message pricing can get expensive at scale * No type safety across the wire — messages are untyped payloads * Not open source (Ably has an open protocol, but the platform is managed) **Best for:** Apps that need reliable, global message delivery without building infrastructure — notifications, live scores, IoT telemetry. Less suited for stateful multiplayer where you need server-side logic. ## Quick Comparison [Section titled “Quick Comparison”](#quick-comparison) | | Zocket | Socket.io | PartyKit | Convex | Liveblocks | Supabase Realtime | | :----------------- | :--------------------- | :---------------- | :-------------------------- | :------------------------ | :------------- | :---------------- | | **Type safety** | ✅ Fully inferred | ⚠️ Manual | ⚠️ Manual | ✅ End-to-end | ✅ SDK-typed | ⚠️ Generated | | **State sync** | ✅ Auto (JSON patches) | ❌ DIY | ❌ DIY | ✅ Auto (reactive queries) | ✅ Auto (CRDTs) | ✅ Auto (DB rows) | | **Server logic** | ✅ Actor methods | ⚠️ Event handlers | ✅ Party class | ✅ Server functions | ❌ Managed only | ⚠️ Edge functions | | **Concurrency** | ✅ Sequential per actor | ❌ DIY | ✅ Single-threaded per party | ✅ Serialized transactions | ✅ CRDT merge | ✅ DB transactions | | **Self-hostable** | ✅ Yes | ✅ Yes | ❌ Cloudflare only | ❌ No | ❌ No | ✅ Yes | | **React bindings** | ✅ Built-in | ⚠️ Community | ⚠️ Community | ✅ Built-in | ✅ Built-in | ✅ Built-in | ## When to use something else [Section titled “When to use something else”](#when-to-use-something-else) * **You need HTTP fallbacks or broad language support** — use Socket.io * **You want zero infrastructure and edge deployment** — use PartyKit * **Your app is data/CRUD-driven and you want a full reactive backend** — use Convex * **You’re building collaborative editing with cursors, comments, and presence** — use Liveblocks * **You need realtime updates from Postgres changes** — use Supabase Realtime * **You need global pub/sub with delivery guarantees** — use Ably or Pusher * **Your app is purely request/response** — use tRPC ## When Zocket Is The Right Fit [Section titled “When Zocket Is The Right Fit”](#when-zocket-is-the-right-fit) Pick Zocket when: * your product is built around live stateful entities instead of mostly stateless endpoints * clients need to call methods on those entities directly * state subscriptions are part of the product, not an afterthought * you want one abstraction instead of stitching together RPC, socket events, and client sync manually That is the core distinction: Zocket is not primarily a transport choice. It is an application model for realtime actors. # Actors > Define stateful actors with typed methods, events, and lifecycle hooks. This page is the API reference for actors. If you want the conceptual background first, read [Why Actors](/motivation/actors/). Actors are the core building block of Zocket. Each actor definition describes a **stateful unit** with a schema-validated state, typed methods, events, and lifecycle hooks. ## Defining an Actor [Section titled “Defining an Actor”](#defining-an-actor) Use the `actor()` function from `@zocket/core`: ```ts import { z } from "zod"; import { actor } from "@zocket/core"; const Counter = actor({ state: z.object({ count: z.number().default(0), }), methods: { increment: { handler: ({ state }) => { state.count += 1; return state.count; }, }, add: { input: z.object({ amount: z.number() }), handler: ({ state, input }) => { state.count += input.amount; return state.count; }, }, }, }); ``` The returned `ActorDef` carries full type information — callers never need to specify generics manually. ## State [Section titled “State”](#state) State is defined using a Standard Schema (Zod, Valibot, etc.). The server initializes state by validating an empty object `{}` against your schema, so use `.default()` for fields: ```ts state: z.object({ players: z.array(PlayerSchema).default([]), phase: z.enum(["lobby", "playing"]).default("lobby"), round: z.number().default(0), }), ``` State is managed with **Immer** on the server. Inside method handlers, you mutate a draft directly — Zocket tracks changes and broadcasts JSON patches to subscribers. ## Methods [Section titled “Methods”](#methods) Each method has an optional `input` schema and a required `handler`: ```ts methods: { // No input reset: { handler: ({ state }) => { state.count = 0; }, }, // With validated input setName: { input: z.object({ name: z.string().min(1) }), handler: ({ state, input }) => { state.name = input.name; }, }, }, ``` ### MethodContext [Section titled “MethodContext”](#methodcontext) Every handler receives a context object: | Property | Type | Description | | :------------- | :--------------------- | :------------------------------------------------------- | | `state` | `TState` (Immer draft) | Mutable state — changes are tracked as patches | | `input` | `InferSchema` | Validated input (or `undefined` if no schema) | | `emit` | `TypedEmitFn` | Emit typed events to subscribers | | `connectionId` | `string` | Opaque ID for the calling connection | | `ctx` | `TCtx` | Middleware context (see [Middleware](/core/middleware/)) | ### Return Values [Section titled “Return Values”](#return-values) Methods can return values. The client receives the return value as a resolved promise: ```ts // Server handler: ({ state }) => { return { count: state.count }; }, // Client const result = await counter.increment(); // { count: 1 } ``` ### Sequential Execution [Section titled “Sequential Execution”](#sequential-execution) All method calls on a single actor instance are **queued and executed sequentially** — one at a time. This gives you single-writer semantics without locks. ## Events [Section titled “Events”](#events) Events are typed messages broadcast to all connections subscribed to an actor instance: ```ts const ChatRoom = actor({ state: z.object({ messages: z.array(MessageSchema).default([]), }), methods: { send: { input: z.object({ text: z.string() }), handler: ({ state, input, emit, connectionId }) => { const msg = { text: input.text, from: connectionId }; state.messages.push(msg); emit("newMessage", msg); }, }, }, events: { newMessage: z.object({ text: z.string(), from: z.string() }), }, }); ``` Event payloads are validated at runtime against their schemas before being broadcast. ## Lifecycle Hooks [Section titled “Lifecycle Hooks”](#lifecycle-hooks) ### Actor Lifecycle [Section titled “Actor Lifecycle”](#actor-lifecycle) Actors support `onActivate` and `onDeactivate` hooks. These fire when an actor instance is first created and before it is destroyed (eviction, shutdown, or redeploy): ```ts const Room = actor({ state: z.object({ createdAt: z.number().default(0), }), methods: { /* ... */ }, onActivate({ state }) { state.createdAt = Date.now(); }, onDeactivate({ state }) { console.log(`Room shutting down, was created at ${state.createdAt}`); }, }); ``` Actor lifecycle hooks receive an `ActorLifecycleContext` with `state` (Immer draft). They are queued alongside method calls to preserve ordering. ### Connection Lifecycle [Section titled “Connection Lifecycle”](#connection-lifecycle) Actors also support `onConnect` and `onDisconnect` hooks. These fire when a connection first interacts with an actor instance and when it disconnects: ```ts const Room = actor({ state: z.object({ online: z.array(z.string()).default([]), }), methods: { /* ... */ }, onConnect({ state, connectionId }) { state.online.push(connectionId); }, onDisconnect({ state, connectionId }) { const idx = state.online.indexOf(connectionId); if (idx !== -1) state.online.splice(idx, 1); }, }); ``` Connection lifecycle hooks receive a `LifecycleContext` with `state` (Immer draft), `connectionId`, and `emit`. They are queued alongside method calls to preserve ordering. Note: middleware `ctx` is **not** available in lifecycle hooks — only in method handlers. ## Full Example: Drawing Room [Section titled “Full Example: Drawing Room”](#full-example-drawing-room) From the `example-draw` package: ```ts import { z } from "zod"; import { actor, createApp } from "@zocket/core"; const Stroke = z.object({ points: z.array(z.tuple([z.number(), z.number()])), color: z.string(), width: z.number(), }); const DrawingRoom = actor({ state: z.object({ players: z.array(z.object({ id: z.string(), name: z.string(), score: z.number(), color: z.string(), connectionId: z.string().default(""), })).default([]), phase: z.enum(["lobby", "drawing", "roundEnd"]).default("lobby"), drawerId: z.string().default(""), word: z.string().default(""), hint: z.string().default(""), strokes: z.array(Stroke).default([]), round: z.number().default(0), }), methods: { join: { input: z.object({ name: z.string() }), handler: ({ state, input, connectionId }) => { const existing = state.players.find((p) => p.name === input.name); if (existing) { existing.connectionId = connectionId; return { playerId: existing.id, color: existing.color }; } const id = Math.random().toString(36).slice(2, 10); const color = ["#ef4444", "#3b82f6", "#22c55e"][state.players.length % 3]; state.players.push({ id, name: input.name, score: 0, color, connectionId }); return { playerId: id, color }; }, }, draw: { input: z.object({ stroke: Stroke }), handler: ({ state, input }) => { state.strokes.push(input.stroke); }, }, guess: { input: z.object({ playerId: z.string(), text: z.string() }), handler: ({ state, input, emit }) => { const correct = input.text.toLowerCase() === state.word.toLowerCase(); if (correct) { emit("correctGuess", { name: input.playerId, word: state.word }); state.phase = "roundEnd"; } return { correct }; }, }, }, events: { correctGuess: z.object({ name: z.string(), word: z.string() }), }, onDisconnect({ state, connectionId }) { const idx = state.players.findIndex((p) => p.connectionId === connectionId); if (idx !== -1) state.players.splice(idx, 1); }, }); export const app = createApp({ actors: { draw: DrawingRoom } }); ``` # Apps > Bundle actor definitions into a typed application. An **App** bundles one or more actor definitions into a single object that the server, client, and React packages can consume. ## Creating an App [Section titled “Creating an App”](#creating-an-app) ```ts import { createApp } from "@zocket/core"; import { ChatRoom } from "./chat"; import { GameMatch } from "./game"; export const app = createApp({ actors: { chat: ChatRoom, game: GameMatch, }, }); ``` The keys you choose (`chat`, `game`) become the **actor names** used everywhere: * **Server** — the handler routes RPC calls to the correct actor by name * **Client** — `client.chat("room-1")` returns a typed handle for the `chat` actor * **React** — `useActor("chat", "room-1")` uses the same name ## Type Inference [Section titled “Type Inference”](#type-inference) The `AppDef` type carries the full type information of all registered actors. Pass `typeof app` as a generic to the client and React factories: ```ts import { createClient } from "@zocket/client"; import type { app } from "./server"; // Full type inference — no manual types needed const client = createClient({ url: "ws://localhost:3000" }); const room = client.chat("general"); // ^? ActorHandle with sendMessage, on("newMessage"), state, etc. ``` ## AppDef Shape [Section titled “AppDef Shape”](#appdef-shape) The returned object has this shape: ```ts interface AppDef { readonly _tag: "AppDef"; readonly actors: TActors; // Record } ``` The `_tag` discriminant is used internally for type narrowing. You generally don’t need to interact with it directly. ## setup() [Section titled “setup()”](#setup) `setup()` is an alias for `createApp()` with different naming, used when registering actors for the distributed runtime (docker-compose deployment with gateway + NATS + runtime): ```ts import { setup } from "@zocket/core"; import { counter } from "./actors/counter"; import { chat } from "./actors/chat"; export const registry = setup({ use: { counter, chat } }); ``` The returned object is the same `AppDef` — the only difference is the naming convention (`use` instead of `actors`). Use `createApp()` for standalone mode and `setup()` for the distributed runtime. ## Naming Conventions [Section titled “Naming Conventions”](#naming-conventions) * Use **camelCase** for actor names: `chat`, `gameMatch`, `drawingRoom` * Each name must be unique within an app * The name is used as-is in the wire protocol (`{ actor: "chat", actorId: "room-1" }`) # Middleware > Chain middleware for authentication, logging, and context enrichment. Middleware lets you run logic **before** every method handler on an actor. Use it for authentication, context enrichment, rate-limiting, or gating. ## Creating Middleware [Section titled “Creating Middleware”](#creating-middleware) ```ts import { middleware } from "@zocket/core"; const authed = middleware() .use(async ({ connectionId }) => { const user = await getUser(connectionId); if (!user) throw new Error("Unauthorized"); return { userId: user.id, role: user.role }; }); ``` Each `.use()` call receives the accumulated context and returns additional context. The types **intersect** — downstream middleware and handlers see all prior context. ## Chaining [Section titled “Chaining”](#chaining) ```ts const admin = middleware() .use(async ({ connectionId }) => { const user = await getUser(connectionId); if (!user) throw new Error("Unauthorized"); return { userId: user.id, role: user.role }; }) .use(({ ctx }) => { if (ctx.role !== "admin") throw new Error("Forbidden"); return { isAdmin: true as const }; }); ``` After this chain, handlers receive `ctx: { userId: string; role: string; isAdmin: true }`. ## Attaching to Actors [Section titled “Attaching to Actors”](#attaching-to-actors) Use `.actor()` instead of the top-level `actor()` function: ```ts const ProtectedRoom = authed.actor({ state: z.object({ messages: z.array(z.string()).default([]), }), methods: { send: { input: z.object({ text: z.string() }), handler: ({ state, input, ctx }) => { // ctx.userId is available and typed state.messages.push(`${ctx.userId}: ${input.text}`); }, }, }, }); ``` Actors created via `middleware().actor()` behave identically to `actor()` — they return the same `ActorDef` type and work with `createApp()`. ## MiddlewareArgs [Section titled “MiddlewareArgs”](#middlewareargs) Each middleware function receives: | Property | Type | Description | | :------------- | :------- | :---------------------------------------- | | `ctx` | `TCtx` | Accumulated context from prior middleware | | `connectionId` | `string` | Stable connection identifier | | `actor` | `string` | Actor name being called | | `actorId` | `string` | Actor instance ID | | `method` | `string` | Method name being called | ## Error Handling [Section titled “Error Handling”](#error-handling) If middleware throws, the RPC call is rejected and the method handler never runs. The error message is sent back to the client as part of the `rpc:result`: ```ts .use(async ({ connectionId }) => { const user = await verifyToken(connectionId); if (!user) throw new Error("Unauthorized"); return { user }; }); ``` On the client: ```ts try { await room.send({ text: "hello" }); } catch (err) { console.error(err.message); // "Unauthorized" } ``` ## Example: Auth + Logging [Section titled “Example: Auth + Logging”](#example-auth--logging) ```ts import { middleware } from "@zocket/core"; const withAuth = middleware() .use(async ({ connectionId }) => { const session = await verifySession(connectionId); if (!session) throw new Error("Unauthorized"); return { userId: session.userId }; }); const withLogging = withAuth .use(({ ctx, actor, actorId, method }) => { console.log(`[${ctx.userId}] ${actor}/${actorId}.${method}()`); return {}; }); // Use in actor definition const MyActor = withLogging.actor({ state: z.object({ /* ... */ }), methods: { doSomething: { handler: ({ ctx }) => { // ctx.userId is available }, }, }, }); ``` # Protocol > Wire format, message types, and JSON patches. Zocket uses a JSON-based protocol over standard WebSockets. All messages are plain JSON objects with a `type` discriminant. ## Message Types [Section titled “Message Types”](#message-types) ```ts const MSG = { RPC: "rpc", RPC_RESULT: "rpc:result", EVENT: "event", EVENT_SUB: "event:sub", EVENT_UNSUB: "event:unsub", STATE_SUB: "state:sub", STATE_UNSUB: "state:unsub", STATE_SNAPSHOT: "state:snapshot", STATE_PATCH: "state:patch", }; ``` ## Client → Server [Section titled “Client → Server”](#client--server) ### RPC Call [Section titled “RPC Call”](#rpc-call) Invoke a method on an actor instance: ```json { "type": "rpc", "id": "rpc_1_m3abc", "actor": "chat", "actorId": "room-1", "method": "sendMessage", "input": { "text": "hello" } } ``` The `id` is generated by the client (`rpc_{counter}_{timestamp}`) and used to correlate the response. ### Event Subscribe / Unsubscribe [Section titled “Event Subscribe / Unsubscribe”](#event-subscribe--unsubscribe) ```json { "type": "event:sub", "actor": "chat", "actorId": "room-1" } { "type": "event:unsub", "actor": "chat", "actorId": "room-1" } ``` ### State Subscribe / Unsubscribe [Section titled “State Subscribe / Unsubscribe”](#state-subscribe--unsubscribe) ```json { "type": "state:sub", "actor": "chat", "actorId": "room-1" } { "type": "state:unsub", "actor": "chat", "actorId": "room-1" } ``` Subscribing to state triggers an immediate `state:snapshot` response. ## Server → Client [Section titled “Server → Client”](#server--client) ### RPC Result [Section titled “RPC Result”](#rpc-result) ```json { "type": "rpc:result", "id": "rpc_1_m3abc", "result": { "count": 42 } } ``` On error: ```json { "type": "rpc:result", "id": "rpc_1_m3abc", "error": "Unauthorized" } ``` ### Event [Section titled “Event”](#event) Broadcast to all event subscribers for this actor instance: ```json { "type": "event", "actor": "chat", "actorId": "room-1", "event": "newMessage", "payload": { "text": "hello", "from": "conn_123" } } ``` ### State Snapshot [Section titled “State Snapshot”](#state-snapshot) Full state sent when a client first subscribes: ```json { "type": "state:snapshot", "actor": "chat", "actorId": "room-1", "state": { "messages": [], "online": ["conn_123"] } } ``` ### State Patch [Section titled “State Patch”](#state-patch) Incremental update using JSON Patch (RFC 6902): ```json { "type": "state:patch", "actor": "chat", "actorId": "room-1", "patches": [ { "op": "add", "path": "/messages/-", "value": { "text": "hi" } } ] } ``` ## JSON Patch Operations [Section titled “JSON Patch Operations”](#json-patch-operations) Zocket generates three patch operations from Immer diffs: | Operation | Description | | :-------- | :---------------------- | | `add` | Add a value at a path | | `replace` | Replace value at a path | | `remove` | Remove value at a path | Paths follow RFC 6902 pointer syntax: `/messages/0/text`, `/players/-` (append). ## Message Builders [Section titled “Message Builders”](#message-builders) The `@zocket/core` package exports builder functions for constructing messages: ```ts import { rpcCall, rpcResult, event, stateSub, stateSnapshot, statePatch } from "@zocket/core/protocol"; // Client builds: const msg = rpcCall("chat", "room-1", "sendMessage", { text: "hi" }); // Server builds: const result = rpcResult(msg.id, { ok: true }); const snap = stateSnapshot("chat", "room-1", currentState); const patch = statePatch("chat", "room-1", [{ op: "add", path: "/count", value: 1 }]); ``` ## Parsing [Section titled “Parsing”](#parsing) ```ts import { parseMessage } from "@zocket/core/protocol"; const msg = parseMessage(rawString); // Returns typed ClientMessage | ServerMessage | null ``` Returns `null` for invalid JSON or messages missing a `type` field. # Types > Type utilities for inference, handles, and client API shapes. Zocket’s type system is built around inference — you define actors once and the types flow through to the client and React layers automatically. The `@zocket/core` package exports several utility types for advanced use cases. ## InferSchema [Section titled “InferSchema”](#inferschema) Extract the output type from any Standard Schema: ```ts import type { InferSchema } from "@zocket/core"; import { z } from "zod"; const UserSchema = z.object({ name: z.string(), age: z.number() }); type User = InferSchema; // { name: string; age: number } ``` ## InferState [Section titled “InferState”](#inferstate) Extract the state type from an `ActorDef`: ```ts import type { InferState } from "@zocket/core"; type ChatState = InferState; // { messages: { text: string; from: string }[] } ``` ## InferMethods [Section titled “InferMethods”](#infermethods) Map an actor’s method definitions to callable signatures: ```ts import type { InferMethods } from "@zocket/core"; type ChatMethods = InferMethods; // { // sendMessage: (input: { text: string }) => Promise; // getHistory: () => Promise; // } ``` Methods with an `input` schema produce `(input: T) => Promise`. Methods without input produce `() => Promise`. ## InferEvents [Section titled “InferEvents”](#inferevents) Map event definitions to callback signatures: ```ts import type { InferEvents } from "@zocket/core"; type ChatEvents = InferEvents; // { // newMessage: (payload: { text: string; from: string }) => void; // } ``` ## ActorHandle [Section titled “ActorHandle”](#actorhandle) The client-facing typed handle for an actor instance. This is what `client.chat("room-1")` returns: ```ts type ActorHandle = InferMethods & { on: >( event: K, callback: InferEvents[K], ) => Unsubscribe; state: { subscribe: (listener: (state: InferState) => void) => Unsubscribe; getSnapshot: () => InferState | undefined; }; meta: ActorHandleMeta; }; interface ActorHandleMeta { name: string; id: string; dispose: () => void; } ``` ## ClientApi [Section titled “ClientApi”](#clientapi) Maps an `AppDef` to the client’s top-level API shape: ```ts type ClientApi = { [K in keyof T["actors"]]: (id: string) => ActorHandle; }; ``` So for an app with `{ actors: { chat: ChatRoom, game: GameMatch } }`, the client type is: ```ts { chat: (id: string) => ActorHandle; game: (id: string) => ActorHandle; } ``` ## TypedEmitFn [Section titled “TypedEmitFn”](#typedemitfn) Constrains event names and payload types to declared events: ```ts type TypedEmitFn = ( event: K, payload: InferSchema, ) => void; ``` Used internally by `MethodContext`. The `emit` function in handlers is automatically typed. ## EventPayload [Section titled “EventPayload”](#eventpayload) Extract the payload type from a specific event: ```ts import type { EventPayload } from "@zocket/core"; type MsgPayload = EventPayload; // { text: string; from: string } ``` ## Wire Protocol Types [Section titled “Wire Protocol Types”](#wire-protocol-types) For low-level protocol work, these types are also exported: ```ts import type { RpcCallMessage, RpcResultMessage, EventMessage, StateSnapshotMessage, StatePatchMessage, JsonPatchOp, ClientMessage, ServerMessage, } from "@zocket/core/types"; ``` See the [Protocol](/core/protocol/) page for details on each message format. # Gateway > WebSocket edge for Zocket — authenticates sessions and bridges WS ↔ NATS. `@zocket/gateway` is the WebSocket edge. Clients connect here. It authenticates each session with the control plane, publishes client messages onto the JetStream `INBOUND` work-queue, and fans outbound messages back to the right WebSocket. ## What it does [Section titled “What it does”](#what-it-does) 1. Accepts WebSocket connections on `PORT` (default `3000`). 2. On upgrade it calls `POST /api/registry/authorize` on the control plane with the request host and bearer token. 3. If authorized, it creates an **ephemeral ordered consumer** on the `OUTBOUND` stream filtered to this one session’s subject (`outbound.{workspaceId}.{projectId}.{sessionId}`) and starts piping messages to the WebSocket. 4. Every client frame is wrapped in an `InboundEnvelope` and published to `inbound.{ws}.{proj}.{actorType}.{actorId}` on the `INBOUND` stream. 5. On connect/disconnect it publishes `session.connected.*` / `session.disconnected.*` on core NATS so runtimes can clean up per-session subscriptions. The gateway is **stateless**. You can run as many as you need behind a load balancer; each WebSocket is pinned to one gateway for its lifetime, but different sessions can land on any instance. ## Environment variables [Section titled “Environment variables”](#environment-variables) | Variable | Default | Description | | ------------------------------ | --------------------------- | ------------------------------------------ | | `PORT` | `3000` | HTTP / WebSocket port | | `NATS_URL` | `nats://localhost:4222` | NATS cluster URL | | `CONTROL_PLANE_URL` | `http://localhost:3000` | Control-plane base URL | | `CONTROL_PLANE_INTERNAL_TOKEN` | `zocket-internal-dev-token` | Bearer token for `/api/registry/authorize` | There are no CLI flags — configuration is env-var only. ## Running it [Section titled “Running it”](#running-it) The simplest local run: ```bash bun ./packages/gateway/src/index.ts ``` In Docker the image is built from `packages/gateway/Dockerfile`. Set the env vars on the container. ```yaml # docker-compose.yml excerpt gateway: build: ./packages/gateway ports: - "3001:3000" environment: NATS_URL: nats://nats:4222 CONTROL_PLANE_URL: http://control-plane:3000 CONTROL_PLANE_INTERNAL_TOKEN: ${INTERNAL_TOKEN} depends_on: - nats ``` ## The `/api/registry/authorize` contract [Section titled “The /api/registry/authorize contract”](#the-apiregistryauthorize-contract) Your control plane implements the authorize endpoint. The gateway calls it like this: ```http POST /api/registry/authorize Authorization: Bearer $CONTROL_PLANE_INTERNAL_TOKEN Content-Type: application/json { "host": "my-app.zocket.app", "token": "" } ``` And expects this response: ```ts { workspaceId: string; projectId: string; userId: string | null; claims: Record; } ``` The returned `userId` and `claims` are attached to the `Connection` and forwarded to actors in `ctx`, so actor code can check authorization without reimplementing auth. The platform reference implementation supports three modes per project: `none`, `jwt-secret`, and `jwt-jwks`. ## Scaling notes [Section titled “Scaling notes”](#scaling-notes) * **Horizontal scaling** — add more gateway replicas. NATS fans out session traffic correctly via filter subjects. * **Sticky sessions** — not required; the outbound consumer uses a session-scoped filter subject, so any gateway can serve any session (as long as it was the one that accepted the WebSocket). * **Connection limits** — tuning is a Bun / kernel concern. The gateway itself is I/O-bound and holds no per-session state beyond the open socket and its NATS consumer. # NATS / JetStream > Stream and consumer configuration for @zocket/nats-transport. Zocket uses NATS JetStream as its messaging fabric. Two streams carry actor traffic; two core-NATS subjects carry session lifecycle. ## Requirements [Section titled “Requirements”](#requirements) * NATS Server 2.10+ with JetStream enabled. * The `nats` container in `docker-compose.yml` uses `nats:2.10-alpine` with `--jetstream --store_dir /data`, which is the minimum for local dev. Production deployments should run a clustered NATS setup with file storage for durability; Zocket’s streams currently default to memory storage and short max-age, which works because the system is designed for at-least-once delivery with short retention rather than long-lived queues. ## Streams [Section titled “Streams”](#streams) `@zocket/nats-transport` ships an `ensureStreams(jsm)` helper that creates both streams if they don’t exist. It’s idempotent — safe to call from every gateway and every runtime at boot. | Stream | Subjects | Retention | Storage | Max age | | ---------- | ------------ | --------- | ------- | ------- | | `INBOUND` | `inbound.>` | Workqueue | Memory | 60s | | `OUTBOUND` | `outbound.>` | Limits | Memory | 30s | **Why workqueue for INBOUND?** Every inbound message is consumed exactly once by the runtime that owns the actor type. Workqueue retention drops the message after ack, preventing redelivery to other consumers. **Why limits for OUTBOUND?** Outbound messages are per-session — if the session’s gateway consumer is slow or disconnected, we want the stream to age out old messages rather than back up indefinitely. 30 seconds is a generous upper bound for “client is briefly behind”; anything older is stale state the client should re-fetch by re-subscribing. ## Subject patterns [Section titled “Subject patterns”](#subject-patterns) | Subject | Who publishes | Who consumes | | ---------------------------------------------- | --------------------------- | ------------------------------------------------ | | `inbound.{ws}.{proj}.{actorType}.{actorId}` | Gateway (on client message) | Runtime (durable consumer per actor type) | | `outbound.{ws}.{proj}.{sessionId}` | Runtime `VirtualConnection` | Gateway (ephemeral ordered consumer per session) | | `session.connected.{ws}.{proj}` (core NATS) | Gateway on WS open | — | | `session.disconnected.{ws}.{proj}` (core NATS) | Gateway on WS close | Runtime (per-session cleanup) | Placeholders: `{ws}` workspaceId · `{proj}` projectId · `{actorType}` actor class name · `{actorId}` actor instance id · `{sessionId}` WebSocket session id. ## Consumers [Section titled “Consumers”](#consumers) Runtime consumers are created via `ensureConsumer(jsm, stream, config)`: | Field | Value | | ----------------- | ------------------------------------------ | | `durable_name` | `rt-{workspaceId}-{projectId}-{actorType}` | | `filter_subject` | `inbound.{ws}.{proj}.{actorType}.>` | | `ack_policy` | `Explicit` | | `max_ack_pending` | `256` | Gateway consumers are ephemeral (no `durable_name`) and ordered, filtered to exactly `outbound.{ws}.{proj}.{sessionId}`. They disappear when the WebSocket closes. ## Sizing and tuning [Section titled “Sizing and tuning”](#sizing-and-tuning) * **`max_ack_pending`** caps how many messages an actor type can be processing concurrently across all instances. Raise it for high-throughput actor types; lower it to apply backpressure earlier. * **Memory storage** means every NATS restart loses un-acked inbound work. For stricter guarantees switch `storage` to `File` in the stream config; you’ll trade latency for durability. * **Max age** is a tradeoff. Raising `OUTBOUND` beyond 30s lets clients tolerate longer disconnects but buffers more state patches. If clients can re-subscribe cheaply, keep it short. ## Operational tips [Section titled “Operational tips”](#operational-tips) * Every gateway and every runtime calls `ensureStreams()` at boot, so you don’t have to pre-provision streams manually. * Use `nats stream ls` and `nats consumer ls INBOUND` against the cluster to inspect what Zocket has set up. * Session lifecycle runs on core NATS, not JetStream — don’t look for `session.*` messages in stream state. # Deployment overview > How Zocket runs in production — gateway, NATS JetStream, and the distributed runtime. A single-process Zocket app (Bun adapter + WebSocket) is all you need to get started. For multi-region, multi-tenant, or hosted deployments you split the system into three processes connected by NATS JetStream. ## The three roles [Section titled “The three roles”](#the-three-roles) ```plaintext Client ─► Gateway ─► NATS JetStream ─► Runtime ─► Actor code ▲ │ │ └──── OUTBOUND ◄──────┴────────────────┘ ``` | Process | Package | Responsibility | | -------------------- | ------------------------ | ---------------------------------------------------------------- | | **Gateway** | `@zocket/gateway` | Terminates WebSockets, authenticates sessions, bridges WS ↔ NATS | | **Runtime** | `@zocket/runtime` | Hosts actor instances, executes methods, publishes state patches | | **NATS / JetStream** | `@zocket/nats-transport` | Durable work-queue (inbound) + per-session outbound buffer | The **control plane** (the hosted platform or your own build) sits alongside and handles auth, project config, and bundle storage. Gateways and runtimes call it over HTTP, not NATS. See the [architecture page](/architecture.html) for the full NATS subject map and actor execution model. ## What goes where [Section titled “What goes where”](#what-goes-where) * All three processes share one NATS cluster. * Gateways are stateless and horizontally scalable — add more to handle more WebSockets. * Runtimes are keyed by project + deployment. A project typically has one runtime (scaled vertically), but NATS work-queue semantics mean you can run multiple runtimes behind the same consumer name if your actors are stateless enough to tolerate instance migration. * The control plane is the only component that talks to your database and object store. ## Local development with Docker Compose [Section titled “Local development with Docker Compose”](#local-development-with-docker-compose) `docker-compose.yml` at the repo root spins up NATS + gateway + runtime for local testing. ```bash docker compose up ``` This starts: * **nats** — `nats:2.10-alpine` with JetStream enabled (`--jetstream --store_dir /data`), ports `4222` (client) and `8222` (monitoring) * **gateway** — built from `packages/gateway/Dockerfile`, exposed on host port `3001` * **runtime** — built from `packages/runtime/Dockerfile`, exposed on host port `8080` You’ll still need a running control plane (your platform instance or the hosted one) — set `CONTROL_PLANE_URL` on both gateway and runtime. ## Minimal env-var cheatsheet [Section titled “Minimal env-var cheatsheet”](#minimal-env-var-cheatsheet) | Variable | Gateway | Runtime | Notes | | ------------------------------ | :-----: | :----------: | -------------------------------------------------------- | | `NATS_URL` | ✓ | ✓ | Default `nats://localhost:4222` | | `CONTROL_PLANE_URL` | ✓ | ✓ | Default `http://localhost:3000` | | `CONTROL_PLANE_INTERNAL_TOKEN` | ✓ | ✓ | Shared secret for `/api/internal/*` calls | | `PORT` | ✓ | | HTTP port, default `3000` | | `API_PORT` | | ✓ | Health/report API, default `8080` | | `BUNDLE_DIR` | | ✓ | Where bundles are stored on disk, default `/app/bundles` | | `DEPLOYMENT_ID` | | **required** | Fails to start if unset | | `WORKSPACE_ID` / `PROJECT_ID` | | ✓ | Default to `local-workspace` / `local-project` | Full reference on the [Gateway](/deployment/gateway/), [Runtime](/deployment/runtime/), and [NATS transport](/deployment/nats/) pages. ## Next steps [Section titled “Next steps”](#next-steps) * [Gateway configuration](/deployment/gateway/) * [Runtime configuration](/deployment/runtime/) * [NATS / JetStream setup](/deployment/nats/) # Runtime > Hosts actor instances, executes methods, and publishes state patches. `@zocket/runtime` is where your actor code actually runs. It fetches a deployment bundle from the control plane, registers a durable JetStream consumer per actor type, executes methods in per-instance FIFO queues, and fans outbound messages back to subscribed sessions. ## What it does on boot [Section titled “What it does on boot”](#what-it-does-on-boot) 1. Reads env vars (see below) — fails fast if `DEPLOYMENT_ID` is missing. 2. Calls `GET /api/internal/runtime/active?workspaceId=&projectId=&deploymentId=` to fetch the bundle URL from the control plane. 3. Downloads the bundle to `{BUNDLE_DIR}/bundle-{timestamp}.mjs` and dynamically imports it. The bundle must export an `AppDef` as `registry`, `app`, or the default export. 4. For each actor type in the app, creates a durable push consumer on the `INBOUND` stream: * **name** — `rt-{workspaceId}-{projectId}-{actorType}` * **filter** — `inbound.{workspaceId}.{projectId}.{actorType}.>` * **ack policy** — explicit, `maxAckPending: 256` 5. Subscribes to `session.disconnected.{ws}.{proj}` on core NATS to clean up per-session actor subscriptions. 6. Starts an Elysia HTTP server on `API_PORT` (default `8080`) serving `GET /health`. 7. Reports `status: "ready"` (with the list of actor types) to `POST /api/internal/runtime/report` on the control plane. On boot errors the runtime still reports back, with `status: "failed"` and a message, so the platform can surface the error in the deployments dashboard. ## Environment variables [Section titled “Environment variables”](#environment-variables) | Variable | Required | Default | Description | | ------------------------------ | :------: | --------------------------- | ------------------------------------------------- | | `DEPLOYMENT_ID` | ✓ | — | Which deployment to load. Runtime exits if unset. | | `NATS_URL` | | `nats://localhost:4222` | NATS cluster URL | | `CONTROL_PLANE_URL` | | `http://localhost:3000` | Control-plane base URL | | `CONTROL_PLANE_INTERNAL_TOKEN` | | `zocket-internal-dev-token` | Bearer for `/api/internal/*` | | `WORKSPACE_ID` | | `local-workspace` | Scopes NATS subjects and control-plane calls | | `PROJECT_ID` | | `local-project` | Scopes NATS subjects and control-plane calls | | `API_PORT` | | `8080` | Health / report HTTP port | | `BUNDLE_DIR` | | `/app/bundles` | Where bundles are downloaded to | ## How actor dispatch works [Section titled “How actor dispatch works”](#how-actor-dispatch-works) One consumer per actor type receives messages for **all** actor IDs of that type, interleaved. A dispatcher routes each message to the right in-memory actor instance by `{actorId}`, creating it lazily on first hit (`onActivate` fires once). Each instance has a FIFO queue and a `processing` flag; the runtime drains the queue one message at a time, so **the same actor instance never runs concurrently** — different instances do. State is held in memory per instance via Immer drafts, diffed into JSON Patches on mutation, and **not persisted**. A restart loses in-memory state and rebuilds it as messages arrive again (JetStream redelivers unacked work). State and event subscriptions are pure in-memory `Set` references on the actor instance — they are not NATS subjects. When an actor mutates state, the runtime iterates the `stateSubscribers` set and publishes one outbound message per subscriber to `outbound.{ws}.{proj}.{sessionId}`. See the [architecture page](/architecture.html) for a deep dive. ## Health and reporting [Section titled “Health and reporting”](#health-and-reporting) The runtime exposes a single HTTP endpoint for orchestrators: ```http GET /health → 200 { "ok": true, "deploymentId": "...", "actorTypes": [...], "deployCount": 1, "workspaceId": "...", "projectId": "..." } ``` It reports deployment state back to the control plane at boot: ```http POST /api/internal/runtime/report Authorization: Bearer $CONTROL_PLANE_INTERNAL_TOKEN Content-Type: application/json { "deploymentId": "...", "status": "ready", "actorTypes": ["chat","counter"] } ``` (or `status: "failed"` with a `message` on error). ## Running it [Section titled “Running it”](#running-it) ```bash DEPLOYMENT_ID=dpl_... \ WORKSPACE_ID=ws_... \ PROJECT_ID=prj_... \ NATS_URL=nats://localhost:4222 \ CONTROL_PLANE_URL=http://localhost:3000 \ bun ./packages/runtime/src/index.ts ``` In Docker the image is built from `packages/runtime/Dockerfile`. Scale vertically before horizontally — multiple runtime replicas on the same durable consumer will compete for messages, which is fine for stateless actors but surprising for actors with in-memory caches. Until you have explicit instance-migration semantics, run one runtime per project per deployment. # Getting Started > Install Zocket and build your first realtime actor in minutes. Zocket is strongest when your product is made of live entities clients interact with directly: chats, agents, sessions, rooms, workflows, or collaborative objects. This quickstart uses a chat room, but the mental model is the same for an AI agent thread, a multiplayer lobby, or a support conversation: define the actor, give it typed methods and state, then let clients call it and subscribe to it. ## Installation [Section titled “Installation”](#installation) * bun ```bash bun add @zocket/core @zocket/server @zocket/client zod ``` * npm ```bash npm install @zocket/core @zocket/server @zocket/client zod ``` For React integration, also add `@zocket/react`: ```bash bun add @zocket/react ``` ## 1. Define a Live Actor [Section titled “1. Define a Live Actor”](#1-define-a-live-actor) Actors are stateful units with identity, typed methods, state, events, and lifecycle hooks. chat.ts ```ts import { z } from "zod"; import { actor, createApp } from "@zocket/core"; const ChatRoom = actor({ state: z.object({ messages: z.array(z.object({ from: z.string(), text: z.string(), })).default([]), }), methods: { sendMessage: { input: z.object({ from: z.string(), text: z.string() }), handler: ({ state, input }) => { state.messages.push(input); }, }, }, }); export const app = createApp({ actors: { chat: ChatRoom } }); ``` ## 2. Serve the Actor [Section titled “2. Serve the Actor”](#2-serve-the-actor) server.ts ```ts import { serve } from "@zocket/server/bun"; import { app } from "./chat"; const server = serve(app, { port: 3000 }); console.log(`Zocket on ws://localhost:${server.port}`); ``` ## 3. Connect a Client to It [Section titled “3. Connect a Client to It”](#3-connect-a-client-to-it) client.ts ```ts import { createClient } from "@zocket/client"; import type { app } from "./chat"; const client = createClient({ url: "ws://localhost:3000" }); // Get a typed handle for a specific live actor instance const room = client.chat("general"); // Call methods — fully typed await room.sendMessage({ from: "Alice", text: "Hello!" }); // Subscribe to state changes room.state.subscribe((state) => { console.log("Messages:", state.messages); }); // Clean up when done room.meta.dispose(); ``` ## 4. Use with React [Section titled “4. Use with React”](#4-use-with-react) zocket.ts ```tsx import { createClient } from "@zocket/client"; import { createZocketReact } from "@zocket/react"; import type { app } from "./chat"; export const client = createClient({ url: "ws://localhost:3000" }); export const { ZocketProvider, useActor, useActorState } = createZocketReact(); ``` App.tsx ```tsx import { ZocketProvider, useActor, useActorState, client } from "./zocket"; function Chat() { const room = useActor("chat", "general"); const messages = useActorState(room, (s) => s.messages); return (
    {messages?.map((m, i) => (
  • {m.from}: {m.text}
  • ))}
); } export function App() { return ( ); } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [Use Cases](/use-cases/) — where Zocket fits best: chats, agents, workflows, rooms, and collaborative state * [Motivation](/motivation/) — why Zocket is structured this way * [Why Actors](/motivation/actors/) — the conceptual model behind actor-based realtime code * [Actors](/core/actors/) — full API for state, methods, events, and lifecycle hooks * [Middleware](/core/middleware/) — auth, context enrichment, gating * [React Hooks](/react/hooks/) — `useActor`, `useActorState`, `useEvent` * [Multiplayer Draw](/guides/multiplayer-draw/) — complete example walkthrough # Authentication > Middleware-based auth patterns for Zocket actors. Zocket uses middleware to enforce authentication before method handlers run. ## The Pattern [Section titled “The Pattern”](#the-pattern) 1. Create a middleware that verifies the connection 2. Throw if unauthorized — the RPC is rejected and the handler never runs 3. Return context (e.g. `userId`) that downstream handlers can use ```ts import { middleware } from "@zocket/core"; const authed = middleware() .use(async ({ connectionId }) => { // Look up the session/user for this connection const user = await getUserByConnection(connectionId); if (!user) throw new Error("Unauthorized"); return { userId: user.id, role: user.role }; }); ``` ## Using in Actors [Section titled “Using in Actors”](#using-in-actors) ```ts const PrivateRoom = authed.actor({ state: z.object({ messages: z.array(z.object({ userId: z.string(), text: z.string(), })).default([]), }), methods: { send: { input: z.object({ text: z.string() }), handler: ({ state, input, ctx }) => { // ctx.userId is typed and guaranteed to exist state.messages.push({ userId: ctx.userId, text: input.text }); }, }, }, }); ``` ## Role-Based Access [Section titled “Role-Based Access”](#role-based-access) Chain middleware for role checks: ```ts const adminOnly = authed .use(({ ctx }) => { if (ctx.role !== "admin") throw new Error("Forbidden"); return { isAdmin: true as const }; }); const AdminPanel = adminOnly.actor({ state: z.object({ /* ... */ }), methods: { dangerousAction: { handler: ({ ctx }) => { // ctx.userId, ctx.role, ctx.isAdmin all available }, }, }, }); ``` ## JWT Verification Example [Section titled “JWT Verification Example”](#jwt-verification-example) ```ts import { middleware } from "@zocket/core"; import { verify } from "jsonwebtoken"; // Store tokens per connection (e.g., set during an initial "auth" method call) const connectionTokens = new Map(); const jwtAuth = middleware() .use(async ({ connectionId }) => { const token = connectionTokens.get(connectionId); if (!token) throw new Error("No token"); try { const payload = verify(token, process.env.JWT_SECRET!) as { sub: string; role: string; }; return { userId: payload.sub, role: payload.role }; } catch { throw new Error("Invalid token"); } }); ``` ## Client-Side Error Handling [Section titled “Client-Side Error Handling”](#client-side-error-handling) When middleware throws, the client’s RPC promise rejects with the error message: ```ts try { await room.send({ text: "hello" }); } catch (err) { if (err.message === "Unauthorized") { // Redirect to login } } ``` ## Actors Without Auth [Section titled “Actors Without Auth”](#actors-without-auth) Actors created with the plain `actor()` function (not through middleware) have no authentication — all connections can call all methods. Use this for public actors like lobbies or status pages. # Multiplayer Draw > Full walkthrough of the example-draw package — a multiplayer drawing game built with Zocket. This guide walks through `packages/example-draw`, a complete multiplayer drawing & guessing game built with Zocket. ## Overview [Section titled “Overview”](#overview) Players join a room, take turns drawing a secret word on a shared canvas, and others try to guess it. The game demonstrates: * Actor state with complex schemas (players, strokes, phases) * Typed methods with input validation * Events (`correctGuess`) * Lifecycle hooks (`onDisconnect` to clean up players) * React hooks (`useActor`, `useActorState`, `useEvent`) ## 1. Game Actor [Section titled “1. Game Actor”](#1-game-actor) The `DrawingRoom` actor manages all game state: game.ts ```ts import { z } from "zod"; import { actor, createApp } from "@zocket/core"; const Stroke = z.object({ points: z.array(z.tuple([z.number(), z.number()])), color: z.string(), width: z.number(), }); export const DrawingRoom = actor({ state: z.object({ players: z.array(z.object({ id: z.string(), name: z.string(), score: z.number(), color: z.string(), connectionId: z.string().default(""), })).default([]), phase: z.enum(["lobby", "drawing", "roundEnd"]).default("lobby"), drawerId: z.string().default(""), word: z.string().default(""), hint: z.string().default(""), strokes: z.array(Stroke).default([]), guesses: z.array(z.object({ playerId: z.string(), name: z.string(), text: z.string(), correct: z.boolean(), })).default([]), round: z.number().default(0), maxRounds: z.number().default(3), }), methods: { join: { input: z.object({ name: z.string() }), handler: ({ state, input, connectionId }) => { // Reconnect if player exists const existing = state.players.find((p) => p.name === input.name); if (existing) { existing.connectionId = connectionId; return { playerId: existing.id, color: existing.color }; } // New player const playerId = Math.random().toString(36).slice(2, 10); const color = PLAYER_COLORS[state.players.length % PLAYER_COLORS.length]; state.players.push({ id: playerId, name: input.name, score: 0, color, connectionId, }); return { playerId, color }; }, }, startRound: { handler: ({ state }) => { if (state.players.length < 2) throw new Error("Need at least 2 players"); state.round += 1; state.strokes = []; state.guesses = []; state.drawerId = state.players[(state.round - 1) % state.players.length].id; state.word = pickRandom(WORDS); state.hint = generateHint(state.word); state.phase = "drawing"; }, }, draw: { input: z.object({ stroke: Stroke }), handler: ({ state, input }) => { if (state.phase === "drawing") state.strokes.push(input.stroke); }, }, guess: { input: z.object({ playerId: z.string(), text: z.string() }), handler: ({ state, input, emit }) => { if (state.phase !== "drawing") return { correct: false }; const player = state.players.find((p) => p.id === input.playerId); if (!player) return { correct: false }; const correct = input.text.trim().toLowerCase() === state.word.toLowerCase(); state.guesses.push({ playerId: input.playerId, name: player.name, text: correct ? "Guessed correctly!" : input.text, correct, }); if (correct) { player.score += 10; emit("correctGuess", { name: player.name, word: state.word }); state.phase = "roundEnd"; } return { correct }; }, }, }, events: { correctGuess: z.object({ name: z.string(), word: z.string() }), }, onDisconnect({ state, connectionId }) { const idx = state.players.findIndex((p) => p.connectionId === connectionId); if (idx === -1) return; const wasDrawer = state.players[idx].id === state.drawerId; state.players.splice(idx, 1); if (wasDrawer && state.phase === "drawing") { state.phase = "lobby"; state.word = ""; state.strokes = []; } }, }); export const app = createApp({ actors: { draw: DrawingRoom } }); ``` Tip The `onDisconnect` hook handles player cleanup and resets the game if the drawer leaves mid-round. ## 2. Server [Section titled “2. Server”](#2-server) The entire server is two lines: server.ts ```ts import { serve } from "@zocket/server/bun"; import { app } from "./game"; const server = serve(app, { port: 3001 }); console.log(`Zocket server on ws://localhost:${server.port}`); ``` ## 3. Client Setup [Section titled “3. Client Setup”](#3-client-setup) src/zocket.ts ```ts import { createClient } from "@zocket/client"; import { createZocketReact } from "@zocket/react"; import type { app } from "../game"; export const client = createClient({ url: "ws://localhost:3001", }); export const { ZocketProvider, useClient, useActor, useEvent, useActorState, } = createZocketReact(); ``` ## 4. React Components [Section titled “4. React Components”](#4-react-components) ### App Shell [Section titled “App Shell”](#app-shell) src/App.tsx ```tsx function RoomView() { const roomId = window.location.hash.slice(1) || "room-1"; const room = useActor("draw", roomId); const phase = useActorState(room, (s) => s.phase); const [playerId, setPlayerId] = useState(null); if (!phase || phase === "lobby") { return ; } return ; } export function App() { return ( ); } ``` ### State Selectors [Section titled “State Selectors”](#state-selectors) Components subscribe to exactly the state they need: ```tsx // Only re-renders when phase changes const phase = useActorState(room, (s) => s.phase); // Only re-renders when players array changes const players = useActorState(room, (s) => s.players); // Only re-renders when strokes change (for the canvas) const strokes = useActorState(room, (s) => s.strokes); ``` ### Event Handling [Section titled “Event Handling”](#event-handling) ```tsx // Show a toast when someone guesses correctly useEvent(room, "correctGuess", ({ name, word }) => { toast(`${name} guessed "${word}"!`); }); ``` ## Key Takeaways [Section titled “Key Takeaways”](#key-takeaways) 1. **One actor = one game room** — state, methods, events, and lifecycle in a single definition 2. **Sequential execution** — no race conditions on guesses or draws 3. **Selective subscriptions** — components only re-render for the state they use 4. **Lifecycle management** — `onDisconnect` handles player cleanup automatically 5. **Two-line server** — `serve(app, { port })` is all you need # State Management > Immer mutations, JSON patches, and client-side subscriptions. Zocket’s state management flows from server-side Immer mutations through JSON patches to client-side subscriptions. ## Server: Immer Mutations [Section titled “Server: Immer Mutations”](#server-immer-mutations) Inside method handlers and lifecycle hooks, `state` is an Immer draft. Mutate it directly: ```ts handler: ({ state, input }) => { // Direct mutations — Immer tracks all changes state.messages.push(input.message); state.count += 1; state.players[0].score = 100; } ``` After the handler returns, Immer generates a list of JSON Patch operations representing the diff. ## Patch Generation [Section titled “Patch Generation”](#patch-generation) Zocket converts Immer patches to RFC 6902 JSON Patches: ```ts // Immer patch: { op: "add", path: ["messages", 2], value: { text: "hi" } } // JSON Patch: { op: "add", path: "/messages/2", value: { text: "hi" } } ``` Only `add`, `replace`, and `remove` operations are generated. ## Broadcast [Section titled “Broadcast”](#broadcast) If any state subscribers are connected, patches are broadcast as `state:patch` messages: ```json { "type": "state:patch", "actor": "chat", "actorId": "room-1", "patches": [ { "op": "add", "path": "/messages/-", "value": { "text": "hi" } } ] } ``` ## Client: State Store [Section titled “Client: State Store”](#client-state-store) Each actor handle has a `StateStore` that maintains a local copy of the state: 1. **Snapshot** — received on first subscription, replaces local state entirely 2. **Patches** — applied incrementally using `structuredClone` + patch logic 3. **Notification** — all subscribers are called after each update ```ts // Subscribe to state changes const unsub = room.state.subscribe((state) => { console.log("Messages:", state.messages); }); // Read current state const current = room.state.getSnapshot(); ``` ## React: Selectors [Section titled “React: Selectors”](#react-selectors) Use `useActorState` with a selector to subscribe to specific parts of state: ```tsx // Only re-renders when `phase` changes const phase = useActorState(room, (s) => s.phase); // Only re-renders when the number of players changes const playerCount = useActorState(room, (s) => s.players.length); // Full state (re-renders on every change) const state = useActorState(room); ``` The selector runs locally on every state update. The hook uses `useSyncExternalStore` for correct concurrent mode behavior. ### Caching [Section titled “Caching”](#caching) The selector result is cached — if the raw state reference hasn’t changed, the previous selected value is reused. This prevents unnecessary re-renders. ## Best Practices [Section titled “Best Practices”](#best-practices) ### Keep State Flat [Section titled “Keep State Flat”](#keep-state-flat) ```ts // Good — flat, easy to patch state: z.object({ players: z.array(PlayerSchema).default([]), phase: z.enum(["lobby", "playing"]).default("lobby"), round: z.number().default(0), }) // Avoid — deeply nested, harder to select efficiently state: z.object({ game: z.object({ round: z.object({ phase: z.string(), players: z.array(/* ... */), }) }) }) ``` ### Use Selectors [Section titled “Use Selectors”](#use-selectors) Don’t subscribe to the full state when you only need one field: ```tsx // Good — minimal re-renders const phase = useActorState(room, (s) => s.phase); // Avoid — re-renders on every state change const state = useActorState(room); const phase = state?.phase; ``` ### Avoid New References in Selectors [Section titled “Avoid New References in Selectors”](#avoid-new-references-in-selectors) ```tsx // Avoid — creates a new array every time const sorted = useActorState(room, (s) => [...s.players].sort((a, b) => b.score - a.score) ); // Better — select the data, sort in useMemo const players = useActorState(room, (s) => s.players); const sorted = useMemo( () => players ? [...players].sort((a, b) => b.score - a.score) : [], [players] ); ``` # Legacy Documentation (v1) > Documentation for the original Zocket v1 router/procedure-based API. Legacy API This section documents the **old v1 API** based on routers and procedures. The current version of Zocket uses an actor-based architecture. See [Getting Started](/getting-started/) for the new API. These pages are preserved for reference. The v1 API used: * **Routers** with `.outgoing()` and `.incoming()` chains * **Procedures** via `zo.message.input(...).handle(...)` * **Room-based broadcasting** via `send.*.broadcast()` / `.toRoom()` * **Connection context** via `zocket.create({ headers, onConnect })` If you’re starting a new project, use the v2 actor-based API instead. # Client > [Legacy v1] Vanilla TypeScript/JavaScript client usage > **This documents the old v1 API.** See [Creating a Client](/client/creating-a-client/) for the current version. The Zocket client (`@zocket/client`) is a typed WebSocket client that generates methods from your router type. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```typescript import { createZocketClient } from "@zocket/client"; import type { AppRouter } from "./server"; const client = createZocketClient("ws://localhost:3000"); // Listen client.on.chat.message((msg) => console.log(msg)); // Send client.chat.post({ text: "Hi" }); ``` # Client Configuration > [Legacy v1] Reconnection, headers, and lifecycle hooks > **This documents the old v1 API.** See [Creating a Client](/client/creating-a-client/) for the current version. ## Initialization Options [Section titled “Initialization Options”](#initialization-options) ```typescript const client = createZocketClient("ws://localhost:3000", { headers: { authorization: "Bearer token123", }, onOpen: () => console.log("Connected"), onClose: () => console.log("Disconnected"), debug: process.env.NODE_ENV === "development", }); ``` ## Reconnection Logic [Section titled “Reconnection Logic”](#reconnection-logic) ```typescript client.onClose(() => { setTimeout(() => client.reconnect(), 5000); }); ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ```typescript client.onError((error) => { console.error("WebSocket Error:", error); }); ``` Properties: `client.readyState`, `client.lastError`. # Usage Patterns > [Legacy v1] RPC, subscriptions, and type safety > **This documents the old v1 API.** See [Actor Handles](/client/actor-handles/) for the current version. ## Subscriptions (Listening) [Section titled “Subscriptions (Listening)”](#subscriptions-listening) ```typescript const unsubscribe = client.on.system.notification((data) => { toast(data.message); }); unsubscribe(); ``` ## RPC (Request / Response) [Section titled “RPC (Request / Response)”](#rpc-request--response) ```typescript const user = await client.users.get({ id: "123" }); ``` ### Timeouts [Section titled “Timeouts”](#timeouts) Fixed 10-second RPC timeout. If the server never responds, the promise rejects. ## Fire-and-forget [Section titled “Fire-and-forget”](#fire-and-forget) If the handler returns `void`, the client method is typed as `void`: ```ts client.analytics.track({ event: "click" }); ``` # Core Concepts > [Legacy v1] Routers, messages, context, and middleware > **This documents the old v1 API.** See [Getting Started](/getting-started/) for the current version. Zocket is built around a few key concepts that enable its type safety and developer experience. ## Routers [Section titled “Routers”](#routers) A **Router** is the contract between your server and client. It defines the structure of your WebSocket application. A router consists of two main parts: ### Outgoing (`.outgoing()`) [Section titled “Outgoing (.outgoing())”](#outgoing-outgoing) These are definitions of messages that the **Server sends to the Client**. You define the shape of the data using a schema (like Zod). ```typescript .outgoing({ chat: { message: z.object({ text: z.string(), from: z.string() }), typing: z.object({ userId: z.string(), isTyping: z.boolean() }) } }) ``` ### Incoming (`.incoming()`) [Section titled “Incoming (.incoming())”](#incoming-incoming) These are handlers for messages that the **Client sends to the Server**. You define the input schema and the handler function. ```typescript .incoming(({ send }) => ({ chat: { sendMessage: zo.message .input(z.object({ text: z.string() })) .handle(({ ctx, input }) => { // Handle the message }) } })) ``` ## Messages [Section titled “Messages”](#messages) A **Message** (procedure) definition in the incoming router specifies: 1. **Input schema** (optional): validates data sent by the client 2. **Middleware** (optional): augments `ctx` and/or blocks execution 3. **Handler**: runs when the message is received ```typescript zo.message .input(z.object({ roomId: z.string() })) .handle(({ ctx, input }) => { ctx.rooms.join(input.roomId); }); ``` ## Context [Section titled “Context”](#context) The **Context** (`ctx`) is an object that is available in every message handler. It is created when a client connects and persists for the duration of the connection. ```typescript const zo = zocket.create({ headers: z.object({ token: z.string() }), onConnect: (headers, clientId) => { const user = verifyToken(headers.token); return { user, db: getDbConnection() }; } }); ``` ### Built-in Context [Section titled “Built-in Context”](#built-in-context) * `ctx.clientId`: The unique ID of the connected client. * `ctx.rooms`: Room helpers (`join`, `leave`, `has`, `current`). ## Middleware [Section titled “Middleware”](#middleware) Middleware allows you to wrap message handlers with common logic. ```typescript const requireUser = zo.message.use(({ ctx }) => { if (!ctx.user) throw new Error("Unauthorized"); return { user: ctx.user }; }); ``` ## JSON payloads [Section titled “JSON payloads”](#json-payloads) Zocket’s transport is JSON. Prefer JSON-friendly types (strings, numbers, booleans, objects, arrays). # Getting Started > [Legacy v1] Get up and running with Zocket v1 in minutes > **This documents the old v1 API.** See [Getting Started](/getting-started/) for the current version. This guide will walk you through creating a simple real-time application with Zocket. ## Installation [Section titled “Installation”](#installation) Install the core package, the client package, and Zod (or your preferred schema validation library). ```bash bun add @zocket/core @zocket/client zod ``` If you are using React, you can also add the React hooks package: ```bash bun add @zocket/react ``` ## Quick Start [Section titled “Quick Start”](#quick-start) ### 1. Define your Router [Section titled “1. Define your Router”](#1-define-your-router) Create a router on your server. This defines the messages your server can send (`outgoing`) and the messages it can receive (`incoming`). server.ts ```typescript import { z } from "zod"; import { zocket, createBunServer } from "@zocket/core"; const zo = zocket.create({ headers: z.object({ authorization: z.string().default("guest"), }), onConnect: (headers) => { return { userId: headers.authorization }; }, }); export const appRouter = zo .router() .outgoing({ chat: { message: z.object({ text: z.string(), from: z.string() }), }, }) .incoming(({ send }) => ({ chat: { post: zo.message .input(z.object({ text: z.string() })) .handle(({ ctx, input }) => { send.chat .message({ text: input.text, from: ctx.userId, }) .broadcast(); }), }, })); export type AppRouter = typeof appRouter; const handlers = createBunServer(appRouter, zo); Bun.serve({ fetch: handlers.fetch, websocket: handlers.websocket, port: 3000, }); ``` ### 2. Create a Client [Section titled “2. Create a Client”](#2-create-a-client) On the client side, you can now connect to your server with full type safety. client.ts ```typescript import { createZocketClient } from "@zocket/client"; import type { AppRouter } from "./server"; const client = createZocketClient("ws://localhost:3000", { headers: { authorization: "Alice" }, }); client.on.chat.message((data) => { console.log(`${data.from}: ${data.text}`); }); client.chat.post({ text: "Hello world!" }); ``` # Authentication (v1) > [Legacy v1] Secure your WebSocket connections > **This documents the old v1 API.** See [Authentication](/guides/authentication/) for the current version. Authentication happens during the initial connection handshake. Headers are sent as URL query parameters. ### Server Side [Section titled “Server Side”](#server-side) ```typescript const zo = zocket.create({ headers: z.object({ token: z.string() }), onConnect: async (headers) => { const user = await verifyToken(headers.token); if (!user) return { user: null }; return { user }; }, }); ``` ### Client Side [Section titled “Client Side”](#client-side) ```typescript const client = createZocketClient("ws://localhost:3000", { headers: { token: "user-session-token" }, }); ``` # Error Handling (v1) > [Legacy v1] Managing errors on both server and client > **This documents the old v1 API.** See [Getting Started](/getting-started/) for the current version. ## Procedure (RPC) errors [Section titled “Procedure (RPC) errors”](#procedure-rpc-errors) Thrown errors are not serialized back to the client. The client call may reject due to RPC timeout. ### Recommended pattern [Section titled “Recommended pattern”](#recommended-pattern) ```typescript // Server — return a typed result .handle(({ input }) => { if (!isValid(input)) { return { ok: false as const, error: "VALIDATION_FAILED" as const }; } return { ok: true as const }; }) // Client const res = await client.doSomething(/* ... */); if (!res.ok) console.error(res.error); ``` ## Connection errors [Section titled “Connection errors”](#connection-errors) ```typescript client.onError((err) => { console.error("Socket error:", err); }); ``` # Type Inference (v1) > [Legacy v1] Getting the most out of TypeScript > **This documents the old v1 API.** See [Types](/core/types/) for the current version. Always export your router type from the server and import it (as a type) in the client: server.ts ```typescript export type AppRouter = typeof appRouter; // client.ts import type { AppRouter } from "./server"; const client = createZocketClient(...); ``` This keeps your bundle size small — the runtime router code is not imported. # Introduction > [Legacy v1] Type-safe WebSocket library with end-to-end type safety > **This documents the old v1 API.** See [Getting Started](/getting-started/) for the current version. **Zocket** is a type-safe WebSocket library that provides end-to-end type safety between client and server, inspired by tRPC. Build real-time applications with confidence using TypeScript and your favorite schema validation library (Zod, Valibot, or any Standard Schema compatible library). ## Key Features [Section titled “Key Features”](#key-features) * **End-to-end Type Safety**: Full TypeScript inference from server to client. Change your server code, and your client code updates automatically. * **Schema Validation**: Works with Zod, Valibot, and any Standard Schema compatible library to validate inputs and outputs. * **Real-time Rooms**: Built-in support for WebSocket rooms/channels for targeted broadcasting (e.g., chat rooms, notifications). * **Middleware Support**: Composable middleware for authentication, logging, error handling, and context injection. * **Runtime Agnostic**: Server is adapter-based (Bun adapter included today) and the client works anywhere with `WebSocket`. * **Framework Agnostic**: Use with any framework (Next.js, Nuxt, React, Vue) or vanilla JS/TS. ## Why Zocket? [Section titled “Why Zocket?”](#why-zocket) Building real-time applications often involves manually syncing types between the backend and frontend. If you change a message structure on the server, you have to remember to update the client code. **Zocket solves this by inferring types directly from your server router.** ### vs. Socket.io [Section titled “vs. Socket.io”](#vs-socketio) Socket.io is great but lacks built-in end-to-end type safety. You often have to maintain shared interface definitions manually. Zocket gives you the same real-time capabilities (rooms, broadcasting) but with the developer experience of tRPC. ### vs. tRPC [Section titled “vs. tRPC”](#vs-trpc) tRPC is amazing for request/response (HTTP/RPC) flows. Zocket brings that same DX to **WebSockets** and **Event-driven** architectures, where the server can push data to the client at any time, not just in response to a request. # Limitations (v1) > [Legacy v1] Known limitations of the v1 API > **This documents the old v1 API.** See [Getting Started](/getting-started/) for the current version. ## Resilience & Connection Management [Section titled “Resilience & Connection Management”](#resilience--connection-management) * No auto-reconnection * No backoff strategies * No heartbeats/ping-pong ## React State Management [Section titled “React State Management”](#react-state-management) * No caching/deduplication * No window focus refetching * No optimistic updates ## Configuration & Timeouts [Section titled “Configuration & Timeouts”](#configuration--timeouts) * Hardcoded 10-second RPC timeout * JSON serialization only ## Scalability [Section titled “Scalability”](#scalability) * Single-node pub/sub only * No distributed pub/sub adapter ## Testing [Section titled “Testing”](#testing) * Requires full WebSocket server for testing * No mock utilities # Protocol (v1) > [Legacy v1] Message protocol specification > **This documents the old v1 API.** See [Protocol](/core/protocol/) for the current version. Zocket uses a simple, JSON-based protocol over standard WebSockets. ## Transport & Serialization [Section titled “Transport & Serialization”](#transport--serialization) * **Transport**: Standard WebSockets (`ws://` or `wss://`). * **Serialization**: Every message is a JSON-encoded object. ## Connection Lifecycle [Section titled “Connection Lifecycle”](#connection-lifecycle) ### Handshake & Authentication [Section titled “Handshake & Authentication”](#handshake--authentication) Because the browser’s native `WebSocket` API does not support custom HTTP headers, Zocket uses **URL Query Parameters** for initial authentication. 1. The client connects to the server URL 2. Headers defined in `zocket.create({ headers: ... })` are passed as query parameters 3. The server validates the headers against the schema before completing the WebSocket upgrade ### Client ID [Section titled “Client ID”](#client-id) Upon successful connection, the server assigns a unique `clientId`. ### Protocol Versioning [Section titled “Protocol Versioning”](#protocol-versioning) `@zocket/client` automatically appends its version via the `x-zocket-version` query parameter. ## Message Formats [Section titled “Message Formats”](#message-formats) ### Standard Message [Section titled “Standard Message”](#standard-message) ```json { "type": "chat.message", "payload": { "text": "Hello!", "from": "user_1" } } ``` ### RPC (Request/Response) [Section titled “RPC (Request/Response)”](#rpc-requestresponse) **Request (Client → Server):** ```json { "type": "users.getProfile", "payload": { "id": "123" }, "rpcId": "unique-msg-id-456" } ``` **Response (Server → Client):** ```json { "type": "__rpc_res", "payload": { "name": "John" }, "rpcId": "unique-msg-id-456" } ``` ## Internal Mechanisms [Section titled “Internal Mechanisms”](#internal-mechanisms) ### Efficient Broadcasting [Section titled “Efficient Broadcasting”](#efficient-broadcasting) Zocket uses a reserved internal topic `__zocket_all__` for global broadcast using the native adapter’s `publish` method. ### Rooms [Section titled “Rooms”](#rooms) Rooms are implemented using the underlying WebSocket engine’s pub/sub system (`ws.subscribe(roomId)`, `ws.unsubscribe(roomId)`). # React > [Legacy v1] Type-safe hooks for React applications > **This documents the old v1 API.** See [React Setup](/react/setup/) for the current version. ## Setup [Section titled “Setup”](#setup) src/utils/zocket.ts ```tsx import { createZocketReact } from "@zocket/react"; import type { AppRouter } from "path-to-your-server-router-type"; export const zocket = createZocketReact(); ``` ## Provider [Section titled “Provider”](#provider) ```tsx import { createZocketClient } from "@zocket/client"; import { zocket } from "./utils/zocket"; const client = createZocketClient("ws://localhost:3000"); export default function App() { return ( ); } ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```tsx function MyComponent() { const client = zocket.useClient(); const { status } = zocket.useConnectionState(); return
Connection: {status}
; } ``` # React Hooks (v1) > [Legacy v1] useEvent, useConnectionState, and more > **This documents the old v1 API.** See [React Hooks](/react/hooks/) for the current version. ## useClient [Section titled “useClient”](#useclient) ```tsx const client = zocket.useClient(); ``` ## useConnectionState [Section titled “useConnectionState”](#useconnectionstate) ```tsx const { status, lastError } = zocket.useConnectionState(); // status: "connecting" | "open" | "closed" ``` ## useEvent [Section titled “useEvent”](#useevent) ```tsx zocket.useEvent(client.on.chat.message, (msg) => { setMessages(prev => [...prev, msg]); }); ``` ## Data Fetching (TanStack Query) [Section titled “Data Fetching (TanStack Query)”](#data-fetching-tanstack-query) ```tsx import { useQuery } from "@tanstack/react-query"; function Profile({ userId }) { const client = zocket.useClient(); const profile = useQuery({ queryKey: ["users.getProfile", userId], queryFn: () => client.users.getProfile({ id: userId }), }); return
{JSON.stringify(profile.data, null, 2)}
; } ``` # Server > [Legacy v1] Server-side setup and message handling > **This documents the old v1 API.** See [Bun Adapter](/server/bun-adapter/) for the current version. To create a Zocket server, you create: 1. A Zocket instance (`zocket.create(...)`) 2. A router (`zo.router().outgoing(...).incoming(...)`) 3. A server adapter (today: Bun via `createBunServer`) ## Server Initialization [Section titled “Server Initialization”](#server-initialization) ```typescript import { zocket, createBunServer } from "@zocket/core"; import { z } from "zod"; const zo = zocket.create({ headers: z.object({ authorization: z.string().optional() }), onConnect: (headers, clientId) => ({ userId: headers.authorization ?? null }), onDisconnect: (ctx, clientId) => { console.log(`Client disconnected: ${clientId}`); } }); const appRouter = zo .router() .outgoing({ system: { announcement: z.object({ text: z.string() }), }, }) .incoming(({ send }) => ({ system: { announce: zo.message .input(z.object({ text: z.string() })) .handle(({ input }) => { send.system.announcement({ text: input.text }).broadcast(); }), }, })); const handlers = createBunServer(appRouter, zo); Bun.serve({ fetch: handlers.fetch, websocket: handlers.websocket, port: 3000, }); ``` ## Sending Messages [Section titled “Sending Messages”](#sending-messages) The `send` object mirrors your `outgoing` router definition. You can also use `handlers.send` for server push outside handlers. # Adapters > [Legacy v1] Run Zocket on different runtimes > **This documents the old v1 API.** See [Bun Adapter](/server/bun-adapter/) for the current version. Zocket’s server is adapter-based. ## Bun (recommended) [Section titled “Bun (recommended)”](#bun-recommended) ```ts import { zocket, createBunServer } from "@zocket/core"; const zo = zocket.create({ /* ... */ }); const appRouter = zo.router().outgoing({ /* ... */ }).incoming(() => ({})); const handlers = createBunServer(appRouter, zo); Bun.serve({ fetch: handlers.fetch, websocket: handlers.websocket, port: 3000, }); ``` ## Custom adapter [Section titled “Custom adapter”](#custom-adapter) If your runtime provides a WebSocket connection with `send`, `close`, and optional pub/sub, you can wire it via `createServer`: ```ts import { createServer, type ServerAdapter } from "@zocket/core"; const adapter: ServerAdapter = { start({ port, hostname, onUpgrade, onOpen, onMessage, onClose }) { return { port: port ?? 3000, stop() { /* ... */ }, publish(topic, message) { /* optional */ }, }; }, }; const server = createServer(appRouter, zo, adapter, { port: 3000 }); ``` # Broadcasting > [Legacy v1] Targeting messages to specific clients > **This documents the old v1 API.** See [Actors](/core/actors/) for the current version. When you construct a message, you must specify who receives it. ### Broadcast [Section titled “Broadcast”](#broadcast) Sends the message to all connected clients. ```typescript send.chat.message({ ... }).broadcast(); ``` ### Direct Message [Section titled “Direct Message”](#direct-message) Sends the message to specific client IDs. ```typescript send.chat.message({ ... }).to([targetClientId]); ``` ### Rooms [Section titled “Rooms”](#rooms) Sends the message to all clients in a specific room. ```typescript send.chat.message({ ... }).toRoom(["room-123"]); ``` # Context > [Legacy v1] State management per connection > **This documents the old v1 API.** See [Middleware](/core/middleware/) for the current version. Context (`ctx`) persists for the lifetime of a WebSocket connection. ## Creating Context [Section titled “Creating Context”](#creating-context) ```typescript const zo = zocket.create({ headers: z.object({ authorization: z.string().optional(), }), onConnect: async (headers, clientId) => { const user = headers.authorization ? await verifyToken(headers.authorization) : null; return { user, connectedAt: new Date() }; }, }); ``` ## Built-in Context [Section titled “Built-in Context”](#built-in-context) | Property | Type | Description | | :--------- | :--------------- | :----------------------------------- | | `clientId` | `string` | Unique identifier for the connection | | `rooms` | `RoomOperations` | Helper to join/leave rooms | ### Rooms API [Section titled “Rooms API”](#rooms-api) * `join(roomId)` — Add the connection to a room * `leave(roomId)` — Remove from a room * `has(roomId)` — Check membership * `current` — `ReadonlySet` of joined rooms # Middleware (v1) > [Legacy v1] Per-message context augmentation and gating > **This documents the old v1 API.** See [Middleware](/core/middleware/) for the current version. Zocket middleware runs before a handler and can add derived values to `ctx` or block execution by throwing. ## Signature [Section titled “Signature”](#signature) ```ts const withExtras = zo.message.use(({ ctx, payload }) => { return { /* merged into ctx */ }; }); ``` ## Authentication + context narrowing [Section titled “Authentication + context narrowing”](#authentication--context-narrowing) ```ts const requireUser = zo.message.use(({ ctx }) => { if (!ctx.user) throw new Error("UNAUTHORIZED"); return { user: ctx.user }; }); ``` ## Composing middleware [Section titled “Composing middleware”](#composing-middleware) ```ts const requireAdmin = ({ ctx }) => { if (ctx.userRole !== "admin") throw new Error("FORBIDDEN"); return { isAdmin: true as const }; }; const adminMessage = requireUser.use(requireAdmin); ``` ## Notes [Section titled “Notes”](#notes) As of `@zocket/core@0.1.0`, thrown errors are not serialized back to the client. If middleware throws, the client call may reject due to RPC timeout. # Procedures > [Legacy v1] Defining inputs, handlers, and validation > **This documents the old v1 API.** See [Actors](/core/actors/) for the current version. Procedures (incoming messages) are the client → server entry points in your Zocket router. ## Anatomy of a Procedure [Section titled “Anatomy of a Procedure”](#anatomy-of-a-procedure) 1. **Input Validation** (Optional) 2. **Middleware** (Optional) 3. **Handler** (Required) ```typescript const sendMessage = zo.message .use(authMiddleware) .input(z.object({ text: z.string() })) .handle(({ ctx, input }) => { console.log(ctx.clientId, input.text); }); ``` ## Input Validation [Section titled “Input Validation”](#input-validation) ```typescript .input( z.object({ title: z.string().min(1), tags: z.array(z.string()).max(5).optional() }) ) ``` If the input is invalid, the handler will not run. ## Async Handlers [Section titled “Async Handlers”](#async-handlers) Handlers can be asynchronous and return values (RPC style): ```typescript .handle(async ({ ctx, input }) => { const post = await db.post.create({ data: { title: input.title } }); return post; }) ``` # Rooms > [Legacy v1] Managing WebSocket rooms and memberships > **This documents the old v1 API.** See [Actors](/core/actors/) for the current version. Rooms group clients for targeted broadcasting. ### Joining a Room [Section titled “Joining a Room”](#joining-a-room) ```typescript .incoming(({ send }) => ({ joinRoom: zo.message .input(z.object({ roomId: z.string() })) .handle(({ ctx, input }) => { ctx.rooms.join(input.roomId); }) })) ``` ### Leaving a Room [Section titled “Leaving a Room”](#leaving-a-room) ```typescript ctx.rooms.leave("room-123"); ``` ### Checking Membership [Section titled “Checking Membership”](#checking-membership) ```typescript if (ctx.rooms.has("admin-room")) { /* ... */ } ``` ### Sending to a room [Section titled “Sending to a room”](#sending-to-a-room) ```ts send.chat.message({ text: "hello" }).toRoom(["room-123"]); ``` # Routers > [Legacy v1] Organizing your application logic > **This documents the old v1 API.** See [Apps](/core/apps/) for the current version. Routers define the structure of your API and hold your procedures. ## Creating a Router [Section titled “Creating a Router”](#creating-a-router) ```typescript const appRouter = zo.router() .outgoing({ /* Server -> Client messages */ }) .incoming(({ send }) => ({ /* Client -> Server messages */ })); ``` ## Modularizing Routers [Section titled “Modularizing Routers”](#modularizing-routers) Extract sections into helpers and compose with object spread: ```typescript export const chatOutgoing = { chat: { message: z.object({ text: z.string(), from: z.string() }), }, } as const; export function chatIncoming({ send }) { return { chat: { post: zo.message .input(z.object({ text: z.string() })) .handle(({ ctx, input }) => { send.chat.message({ text: input.text, from: ctx.clientId }).broadcast(); }), }, } as const; } export const appRouter = zo .router() .outgoing({ ...chatOutgoing }) .incoming(({ send }) => ({ ...chatIncoming({ send }) })); ``` # Why Zocket? > [Legacy v1] Comparison with Socket.io and raw WebSockets > **This documents the old v1 API.** See [Getting Started](/getting-started/) for the current version. ## vs. Socket.io [Section titled “vs. Socket.io”](#vs-socketio) | Feature | Socket.io | Zocket | | :----------------------- | :------------------------------ | :--------------------------------- | | **Type Safety** | Partial (via manual interfaces) | **End-to-End Inferred** | | **Validation** | Manual | **Built-in (Zod/Valibot)** | | **Developer Experience** | Event strings (`"chat:msg"`) | **Fluent API** (`client.chat.msg`) | In Socket.io, you define interfaces on both client and server manually. Zocket infers types directly from your router. ## vs. Raw WebSockets [Section titled “vs. Raw WebSockets”](#vs-raw-websockets) With Raw WebSockets you have to build protocol parsing, routing, room management, and type safety yourself. Zocket provides structured routing, rooms, and validation out of the box. ## Summary [Section titled “Summary”](#summary) Use **Zocket** if you use TypeScript and want tRPC-like DX for WebSockets. Use **Socket.io** if you need HTTP long-polling fallbacks or use JavaScript without types. # Motivation > Why Zocket is shaped the way it is. Zocket is opinionated on purpose. It does not try to be a thin WebSocket wrapper or a generic event bus. It assumes that many realtime products are built from long-lived, stateful things like chats, agents, rooms, matches, documents, boards, and sessions, and it gives those things a first-class shape. This section explains the thinking behind that design. ## Read This Section If You Want To Understand… [Section titled “Read This Section If You Want To Understand…”](#read-this-section-if-you-want-to-understand) * why Zocket is not just `socket.on("event")` with better TypeScript * why Zocket is better understood as an application model, not just transport * why state sync is built in instead of left to app code * why the core abstraction is an actor instead of a flat message router * what kinds of realtime systems Zocket is actually optimized for ## Start Here [Section titled “Start Here”](#start-here) * [Philosophy](/motivation/philosophy/) explains the broader design goals behind Zocket. * [Why Actors](/motivation/actors/) explains why actors are the core unit in the library. * [Why The Type Safety Is Better](/motivation/type-safety/) explains why inferred, validated types are stronger than shared `types.ts` files. * [Comparison](/comparison/) shows where Zocket fits relative to Socket.io, PartyKit, Convex, Liveblocks, and others. ## The Short Version [Section titled “The Short Version”](#the-short-version) Zocket is built around a simple belief: Realtime applications are usually collections of live product entities, not a flat stream of unrelated socket messages. Once you accept that, a lot of the API shape follows naturally: * define server logic around those stateful things * infer the client API from the server definition * make state sync automatic instead of ad hoc * process changes sequentially per instance so race conditions stay local That is the core idea behind the rest of the docs. # Why Actors > Why Zocket models realtime systems around actors. Actors are the central abstraction in Zocket because they match the natural shape of most realtime systems. ## Realtime Apps Are Usually Collections of Stateful Things [Section titled “Realtime Apps Are Usually Collections of Stateful Things”](#realtime-apps-are-usually-collections-of-stateful-things) Most applications are not one giant socket. They are made from many separate, addressable things: * a chat room * a game match * a drawing canvas * a collaborative document * a presence channel Each of those things usually needs the same capabilities: * it owns state * it exposes methods that mutate that state * it emits events to interested clients * it cares about connections coming and going * it must handle concurrent actions safely That bundle of concerns is exactly what an actor is good at. ## What “Actor” Means in Zocket [Section titled “What “Actor” Means in Zocket”](#what-actor-means-in-zocket) In Zocket, an actor is: * a named definition * instantiated by ID * with its own state * its own methods * its own events * and its own lifecycle So instead of asking: * “what message type just arrived?” you ask: * “which room, match, or document is this for?” That shift matters. It turns your server from a message router into a collection of stateful units with clear ownership. ## Why Not a Flat Bag of Handlers? [Section titled “Why Not a Flat Bag of Handlers?”](#why-not-a-flat-bag-of-handlers) Without an explicit unit like an actor, realtime code often collapses into routing logic: ```ts socket.onmessage = async (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case "join-room": // load room state // check membership // mutate room // broadcast update break; case "send-message": // load room state again // validate input // append message // broadcast event break; } }; ``` This works for a prototype, but it scales poorly in both senses: * the code for one logical thing gets scattered across many handlers * the runtime has no obvious unit to route, isolate, or place The room exists conceptually, but your code does not reflect that. ## What It Looks Like With Actors [Section titled “What It Looks Like With Actors”](#what-it-looks-like-with-actors) With actors, the structure matches the problem: ```ts const ChatRoom = actor({ state: z.object({ messages: z.array(MessageSchema).default([]), members: z.array(z.string()).default([]), }), methods: { join: { input: z.object({ name: z.string() }), handler: ({ state, input }) => { state.members.push(input.name); }, }, sendMessage: { input: z.object({ text: z.string() }), handler: ({ state, input, emit, connectionId }) => { const message = { from: connectionId, text: input.text }; state.messages.push(message); emit("message", message); }, }, }, events: { message: MessageSchema, }, }); ``` Now the room is not an implicit convention. It is the actual unit of code. ## Why This Shape Works Well [Section titled “Why This Shape Works Well”](#why-this-shape-works-well) ### Local reasoning [Section titled “Local reasoning”](#local-reasoning) The state, methods, events, and lifecycle for one realtime thing live together. That makes the code easier to understand and change. ### Single-writer semantics [Section titled “Single-writer semantics”](#single-writer-semantics) One actor instance processes one method at a time. That keeps concurrency problems local and dramatically reduces the amount of locking and coordination app code needs. ### Natural scaling boundaries [Section titled “Natural scaling boundaries”](#natural-scaling-boundaries) Rooms, matches, and documents are already the things you want to route and place in a distributed system. Actors make those boundaries explicit early. ### Better mental model [Section titled “Better mental model”](#better-mental-model) The API stops being “send some event name over the wire” and becomes “talk to this room” or “subscribe to this document.” That is much closer to how developers already think about their app. ## Why Zocket Uses Actors [Section titled “Why Zocket Uses Actors”](#why-zocket-uses-actors) Zocket does not use actors because they sound clever. It uses them because they are the right shape for most realtime problems. If your app has many rooms, many games, or many documents, you already have many actors. Zocket just makes that explicit, typed, and easier to work with. If you want the API details next, continue to the [Actors reference](/core/actors/). # Philosophy > The design principles behind Zocket. Zocket is built on a simple idea: The core units of many modern apps are live, stateful actors. Chats, agents, rooms, sessions, documents, matches, and workflows all have identity, behavior, changing state, and clients connected to them in realtime. Zocket exists to make those things first-class instead of forcing them through stateless APIs plus an ad hoc websocket layer. That is the philosophical distinction behind the rest of the library. Zocket is not just “websocket infrastructure.” It is an application model for building products around live entities. ## The Core Idea [Section titled “The Core Idea”](#the-core-idea) Zocket is for building products around long-lived, stateful actors. Not just sending messages over a socket, but modeling things like: * chat threads * AI agent sessions * collaborative rooms * support conversations * multiplayer matches * workflow runs as entities with: * identity * state * behavior * subscriptions In Zocket, the unit you build around is not an endpoint. It is an actor instance clients can talk to directly. ## Build Around Actors, Not Endpoints [Section titled “Build Around Actors, Not Endpoints”](#build-around-actors-not-endpoints) Traditional backend design starts with routes, handlers, and stateless requests. Zocket starts with entities that live. A chat thread, AI agent, room, document, or multiplayer match is not just a collection of API calls. It has ongoing state, methods that mutate that state, and connected clients that need to stay in sync. So Zocket treats those things as first-class: * you talk to an actor * you invoke typed methods on it * you subscribe to its state * you keep logic attached to the entity that owns it That makes the mental model closer to product building blocks than to “servers that happen to hold sockets.” ## Why Zocket Exists [Section titled “Why Zocket Exists”](#why-zocket-exists) Modern realtime apps are rarely just about transport. The hard part is expressing live application state in a way that feels natural to build, reason about, and evolve over time. Without a stronger abstraction, teams usually end up stitching together three separate concerns: * request handlers or RPC endpoints * websocket plumbing * client-side state management and sync logic That can get a demo working, but it usually turns product concepts into glue code. Zocket exists to make realtime systems feel like application development again: * typed * stateful * composable * direct ## The Problems Zocket Cares About [Section titled “The Problems Zocket Cares About”](#the-problems-zocket-cares-about) ### 1. The wire is usually stringly typed [Section titled “1. The wire is usually stringly typed”](#1-the-wire-is-usually-stringly-typed) With raw sockets or Socket.io-style APIs, server and client are connected by event names and payload conventions. ```ts socket.emit("chat:message", { text: "hello" }); socket.on("chat:mesage", (data) => { console.log(data.txt); }); ``` That looks fine until you rename an event, change a payload, or misspell one side. Then the type system stops helping exactly where you need it most: at the boundary between server and client. Zocket’s answer is to define the shape once on the server and infer the client API from that source of truth. ### 2. State sync is not a side concern [Section titled “2. State sync is not a side concern”](#2-state-sync-is-not-a-side-concern) Most realtime apps are not just “fire an event and forget.” They have shared state: * a room roster * a game board * a document tree * a presence map * a running score With lower-level tools, keeping that state in sync becomes app code: * invent a patch protocol * decide what to rebroadcast * track who is subscribed * recover when the client reconnects Zocket treats state sync as part of the core runtime instead of something every app reimplements. ### 3. Server logic needs a shape [Section titled “3. Server logic needs a shape”](#3-server-logic-needs-a-shape) The hardest part of realtime code is usually not transport. It is organization. Once an app grows beyond a few messages, you need a clear answer to questions like: * where does the state for this room live? * which code owns mutations to that state? * how do concurrent actions get serialized? * where do connection lifecycle hooks belong? * what is the unit we route, scale, and reason about? Zocket’s answer is: make that unit explicit. ## What Zocket Is Not [Section titled “What Zocket Is Not”](#what-zocket-is-not) Zocket is not: * a thin websocket transport layer * a generic pub/sub utility * a bag of event handlers with better autocomplete * a product optimized primarily around raw connection management Zocket is optimized around stateful actors that clients can talk to in realtime. If you think in terms of rooms, agents, sessions, threads, documents, or matches, you are probably already thinking in Zocket’s model. ## The Design Principles [Section titled “The Design Principles”](#the-design-principles) ### Define once, infer everywhere [Section titled “Define once, infer everywhere”](#define-once-infer-everywhere) The server should be the source of truth for: * method names * input types * return types * event payloads * state shape The client and React layers should inherit that automatically. ### Treat state as a product feature [Section titled “Treat state as a product feature”](#treat-state-as-a-product-feature) State synchronization is not a helper utility in a realtime system. It is most of the product experience. So Zocket bakes it in instead of treating it like optional glue code. ### Make the unit of coordination explicit [Section titled “Make the unit of coordination explicit”](#make-the-unit-of-coordination-explicit) A realtime app is usually made from many isolated stateful things. The runtime should expose that directly instead of forcing everything into a global message handler. ### Prefer local reasoning [Section titled “Prefer local reasoning”](#prefer-local-reasoning) The more logic you can keep attached to one room, one document, one agent, or one match, the easier it is to understand, test, and operate. ## Why Not Just Combine REST, WebSockets, And Client State Yourself? [Section titled “Why Not Just Combine REST, WebSockets, And Client State Yourself?”](#why-not-just-combine-rest-websockets-and-client-state-yourself) You can. Zocket exists because that stack often turns one product concept into three separate technical systems: * an API surface for mutations * a websocket channel for updates * a client-side state layer to stitch everything back together Zocket compresses those into one abstraction: * a direct client-to-actor model * typed methods and state * built-in subscriptions and sync * less glue code between product ideas and implementation If you want the deeper reasoning behind the core abstraction, continue to [Why Actors](/motivation/actors/). # Why The Type Safety Is Better > Why inferred, validated types are stronger than manually maintained shared type files. Many realtime tools can be made “type-safe” if you are disciplined enough. You can create a shared `types.ts` file, export interfaces, and make sure both server and client import them. That is better than plain `any`. But it is still not the same thing as Zocket’s type safety. The difference is this: * explicit type files describe what you hope the system does * Zocket’s types are derived from what the system actually does That sounds subtle, but it changes a lot in practice. ## The Easy Example [Section titled “The Easy Example”](#the-easy-example) Imagine a chat room with: * state: a list of messages * one method: `sendMessage` * one event: `newMessage` ### The explicit type file approach [Section titled “The explicit type file approach”](#the-explicit-type-file-approach) You might start with a shared file like this: types.ts ```ts export type Message = { from: string; text: string; }; export interface ChatEvents { newMessage: Message; } export interface ChatMethods { sendMessage: { text: string }; } export interface ChatState { messages: Message[]; } ``` Then the server tries to follow it: server.ts ```ts import type { ChatEvents, ChatMethods, ChatState } from "./types"; const state: ChatState = { messages: [] }; socket.on("sendMessage", (input: ChatMethods["sendMessage"]) => { const msg = { from: socket.id, text: input.text }; state.messages.push(msg); io.emit("newMessage", msg satisfies ChatEvents["newMessage"]); }); ``` And the client tries to follow it too: client.ts ```ts import type { ChatEvents, ChatMethods } from "./types"; socket.emit("sendMessage", { text: "hello" } satisfies ChatMethods["sendMessage"]); socket.on("newMessage", (payload: ChatEvents["newMessage"]) => { console.log(payload.text); }); ``` This looks solid. It is still weaker than Zocket in several important ways. ## Where explicit type files break down [Section titled “Where explicit type files break down”](#where-explicit-type-files-break-down) ### 1. The event names are still just strings [Section titled “1. The event names are still just strings”](#1-the-event-names-are-still-just-strings) The payload may be typed, but `"sendMessage"` and `"newMessage"` are still string conventions. If you rename the server event and forget one client: ```ts socket.on("new-message", (payload: ChatEvents["newMessage"]) => { console.log(payload.text); }); ``` your shared type file does not save you. The payload type is correct. The wire contract is still broken. In Zocket, method names and event names come directly from the actor definition, so the client API changes with the server definition. ## 2. The types can drift from the implementation [Section titled “2. The types can drift from the implementation”](#2-the-types-can-drift-from-the-implementation) A shared type file is a second source of truth. That means the real system now lives in two places: * the implementation * the manually maintained types Those two things drift all the time. For example, someone updates the server logic: ```ts const msg = { from: socket.id, text: input.text, sentAt: Date.now(), }; ``` but forgets to update `Message` in `types.ts`. Now: * the runtime sends `{ from, text, sentAt }` * the shared types still say `{ from, text }` The codebase compiles. The contract is already lying. In Zocket, the shape comes from the schema and handler definitions themselves, so there is no second file to forget. ## 3. Explicit types usually do not validate anything at runtime [Section titled “3. Explicit types usually do not validate anything at runtime”](#3-explicit-types-usually-do-not-validate-anything-at-runtime) TypeScript disappears at runtime. So this client call: ```ts socket.emit("sendMessage", { txt: 123 }); ``` can still happen if the caller is untyped, casted, or coming from another environment. Shared interfaces do not reject bad payloads on the server. Zocket method inputs and event payloads are backed by schemas, so the same definition gives you: * TypeScript inference at compile time * runtime validation at the boundary That is a much stronger guarantee. ## 4. Return types are usually not part of the manual contract [Section titled “4. Return types are usually not part of the manual contract”](#4-return-types-are-usually-not-part-of-the-manual-contract) With raw socket systems, request and response shapes often end up being informal: ```ts socket.emit("sendMessage", { text: "hi" }, (result) => { console.log(result.messageId); }); ``` Now you need another manually maintained type for the callback result. And then another one for errors. And another convention for timeouts. Zocket gets the return type from the method handler itself: ```ts handler: ({ state, input, connectionId }) => { const msg = { id: crypto.randomUUID(), from: connectionId, text: input.text }; state.messages.push(msg); return { messageId: msg.id }; } ``` The client immediately sees: ```ts const result = await room.sendMessage({ text: "hi" }); // { messageId: string } ``` No separate response type file is needed. ## 5. State types are especially easy to fake [Section titled “5. State types are especially easy to fake”](#5-state-types-are-especially-easy-to-fake) In manual systems, people often write: ```ts type ChatState = { messages: Message[]; }; ``` But that does not tell you: * how the state is initialized * whether the server actually conforms to that shape * whether the client snapshot really matches it * whether patches preserve that shape over time It is only a compile-time promise. In Zocket, state comes from the actor’s schema: ```ts state: z.object({ messages: z.array(MessageSchema).default([]), }), ``` That one definition drives: * the server’s state shape * the client’s subscribed state type * the React selector input type * runtime validation of the initial state shape ## 6. Tooling gets worse as your app grows [Section titled “6. Tooling gets worse as your app grows”](#6-tooling-gets-worse-as-your-app-grows) With explicit type files, you have to manually thread types through every layer: * server handlers * emitted events * client wrappers * React hooks * local state selectors That usually leads to helper types like: ```ts type EventMap = { ... }; type MethodInputs = { ... }; type MethodResults = { ... }; type RoomState = { ... }; ``` It works, but the amount of bookkeeping grows with the app. Zocket goes the other direction: you define the actor once, and the rest is inferred. ## The same example in Zocket [Section titled “The same example in Zocket”](#the-same-example-in-zocket) ```ts import { z } from "zod"; import { actor, createApp } from "@zocket/core"; const MessageSchema = z.object({ id: z.string(), from: z.string(), text: z.string(), }); const ChatRoom = actor({ state: z.object({ messages: z.array(MessageSchema).default([]), }), methods: { sendMessage: { input: z.object({ text: z.string() }), handler: ({ state, input, connectionId, emit }) => { const msg = { id: crypto.randomUUID(), from: connectionId, text: input.text, }; state.messages.push(msg); emit("newMessage", msg); return { messageId: msg.id }; }, }, }, events: { newMessage: MessageSchema, }, }); export const app = createApp({ actors: { chat: ChatRoom }, }); ``` From that one definition, Zocket gives you all of this automatically. ## What Zocket infers, one by one [Section titled “What Zocket infers, one by one”](#what-zocket-infers-one-by-one) ### Actor names [Section titled “Actor names”](#actor-names) From: ```ts createApp({ actors: { chat: ChatRoom }, }); ``` You get: ```ts client.chat("general"); client.game("general"); // type error ``` A shared type file usually does not protect this unless you build another explicit API wrapper. ### Method names [Section titled “Method names”](#method-names) From: ```ts methods: { sendMessage: { ... } } ``` You get: ```ts await room.sendMessage({ text: "hi" }); await room.postMessage({ text: "hi" }); // type error ``` ### Method input types [Section titled “Method input types”](#method-input-types) From: ```ts input: z.object({ text: z.string() }) ``` You get: ```ts await room.sendMessage({ text: "hi" }); await room.sendMessage({ txt: "hi" }); // type error await room.sendMessage({ text: 123 }); // type error ``` And at runtime, invalid payloads are rejected too. ### Method return types [Section titled “Method return types”](#method-return-types) From: ```ts return { messageId: msg.id }; ``` You get: ```ts const result = await room.sendMessage({ text: "hi" }); result.messageId; // string result.id; // type error ``` No separate “response type” has to be authored and kept in sync. ### Event names and payloads [Section titled “Event names and payloads”](#event-names-and-payloads) From: ```ts events: { newMessage: MessageSchema, } ``` You get: ```ts room.on("newMessage", (payload) => { payload.text; // string payload.sentAt; // type error }); room.on("message", () => {}); // type error ``` And on the server: ```ts emit("newMessage", msg); // typed and validated emit("message", msg); // type error ``` ### State shape [Section titled “State shape”](#state-shape) From: ```ts state: z.object({ messages: z.array(MessageSchema).default([]), }) ``` You get: ```ts room.state.subscribe((state) => { state.messages[0]?.text; }); ``` And in React: ```tsx const messages = useActorState(room, (s) => s.messages); ``` The selector input is typed from the same actor definition. No extra `ChatState` wiring is required. ### React actor names [Section titled “React actor names”](#react-actor-names) From: ```ts createZocketReact() ``` You get: ```tsx const room = useActor("chat", "general"); const game = useActor("game", "general"); // type error if "game" does not exist ``` Again, this comes from the app definition, not a separately maintained string union. ## Why this is stronger than manual types [Section titled “Why this is stronger than manual types”](#why-this-is-stronger-than-manual-types) Zocket’s type safety is stronger because it combines four things at once: ### 1. Single source of truth [Section titled “1. Single source of truth”](#1-single-source-of-truth) You define the contract in one place: the actor. There is no separate “documentation type layer” to keep in sync with the runtime. ### 2. Inference across layers [Section titled “2. Inference across layers”](#2-inference-across-layers) The same definition flows into: * server handler context * emitted event payloads * client methods * state subscriptions * React hooks Manual type files can imitate parts of this, but only with more boilerplate and more places to drift. ### 3. Runtime validation [Section titled “3. Runtime validation”](#3-runtime-validation) Schemas are not just for inference. They are also checked at runtime. That means the system is safer even when: * someone uses `as any` * a third-party client connects * an old client version sends the wrong shape Explicit interfaces cannot do that on their own. ### 4. The API surface is generated from real code [Section titled “4. The API surface is generated from real code”](#4-the-api-surface-is-generated-from-real-code) In Zocket, the client shape is literally derived from the server definition. That means renaming a method or event changes the client API immediately. With manual types, renaming often means updating: * runtime strings * shared type maps * wrappers * React helpers That is exactly the kind of duplication that creates stale contracts. ## The honest takeaway [Section titled “The honest takeaway”](#the-honest-takeaway) Yes, you can get decent type safety in a raw socket setup with enough discipline. But it is still mostly manual. Zocket is different because its type safety is: * inferred instead of re-declared * validated instead of assumed * connected to the actual runtime instead of a parallel type layer That is why it holds up better as the app grows. If you want the API details behind this system, see [Types](/core/types/) and [Creating a Client](/client/creating-a-client/). # React Hooks > useActor, useEvent, useActorState — typed hooks for realtime React apps. All hooks are generated by `createZocketReact()` and are fully typed from your actor definitions. ## useClient [Section titled “useClient”](#useclient) Returns the typed client from context: ```tsx const client = useClient(); ``` Throws if used outside ``. ## useActor [Section titled “useActor”](#useactor) Get a stable, ref-counted `ActorHandle` for a specific actor instance: ```tsx const room = useActor("chat", roomId); ``` * `actorName` — must match a key in your app’s `actors` (typed and autocompleted) * `actorId` — the instance identifier (e.g. `"room-1"`) The handle is **ref-counted** — multiple components can share the same actor without one unmount killing the other’s connection. On unmount, `meta.dispose()` is called automatically. ```tsx function ChatRoom({ roomId }: { roomId: string }) { const room = useActor("chat", roomId); // Call methods const handleSend = async (text: string) => { await room.sendMessage({ from: "Alice", text }); }; // Subscribe to events room.on("newMessage", (msg) => { console.log(msg); }); return ; } ``` ### Key Identity [Section titled “Key Identity”](#key-identity) The handle is recreated when `actorName` or `actorId` changes. The previous handle is disposed. ### StrictMode Compatibility [Section titled “StrictMode Compatibility”](#strictmode-compatibility) React StrictMode unmounts and remounts components. Zocket handles this by deferring disposal by one tick — the remount re-retains the handle before the deferred dispose fires. ## useEvent [Section titled “useEvent”](#useevent) Subscribe to an actor event with automatic cleanup: ```tsx useEvent(room, "correctGuess", (payload) => { // payload is typed from your event schema toast(`${payload.name} guessed "${payload.word}"!`); }); ``` * Subscribes on mount, unsubscribes on unmount * The callback ref is kept up-to-date (no stale closures) * Equivalent to `useEffect(() => room.on("event", cb), [room])` ### Type Inference Note [Section titled “Type Inference Note”](#type-inference-note) For full type inference on event names and payloads, use `room.on()` directly. The `useEvent` hook accepts `string` for the event name to keep the implementation simple — but the callback payload is still inferred from the handle type. ## useActorState [Section titled “useActorState”](#useactorstate) Subscribe to actor state with an optional selector: ```tsx // Full state const state = useActorState(room); // With selector — only re-renders when messages change const messages = useActorState(room, (s) => s.messages); // Derived value const messageCount = useActorState(room, (s) => s.messages.length); ``` ### How It Works [Section titled “How It Works”](#how-it-works) 1. Uses `useSyncExternalStore` under the hood 2. Subscribes to the handle’s `StateStore` 3. If a selector is provided, it runs locally on each state update 4. Uses **shallow compare** caching — the selector result is only updated when the raw state reference changes 5. Returns `undefined` until the first state snapshot is received ### Selector Best Practices [Section titled “Selector Best Practices”](#selector-best-practices) ```tsx // Good — simple property access const phase = useActorState(room, (s) => s.phase); // Good — derived computation const isMyTurn = useActorState(room, (s) => s.drawerId === myId); // Avoid — creating new arrays on every state change // (will cause re-renders because the reference changes) const sorted = useActorState(room, (s) => [...s.players].sort((a, b) => b.score - a.score) ); // Instead, sort in the component with useMemo ``` ## useConnectionStatus [Section titled “useConnectionStatus”](#useconnectionstatus) Returns the current WebSocket connection status. Re-renders when it changes: ```tsx const status = useConnectionStatus(); // "connecting" | "connected" | "reconnecting" | "disconnected" ``` ```tsx function ConnectionBanner() { const status = useConnectionStatus(); if (status === "reconnecting") { return
Reconnecting...
; } if (status === "disconnected") { return
Disconnected
; } return null; } ``` ## Full Example [Section titled “Full Example”](#full-example) ```tsx import { ZocketProvider, useActor, useEvent, useActorState, client } from "./zocket"; function GameRoom({ roomId }: { roomId: string }) { const room = useActor("draw", roomId); const phase = useActorState(room, (s) => s.phase); const players = useActorState(room, (s) => s.players); useEvent(room, "correctGuess", ({ name, word }) => { alert(`${name} guessed "${word}"!`); }); return (

Phase: {phase}

    {players?.map((p) => (
  • {p.name} — {p.score} pts
  • ))}
); } export function App() { return ( ); } ``` # React Setup > Create typed hooks and providers for your Zocket app. The `@zocket/react` package provides a factory that generates fully typed hooks and a context provider. ## Installation [Section titled “Installation”](#installation) ```bash bun add @zocket/react @zocket/client @zocket/core ``` ## Create Hooks [Section titled “Create Hooks”](#create-hooks) Use `createZocketReact` to generate typed hooks from your app definition: src/zocket.ts ```ts import { createClient } from "@zocket/client"; import { createZocketReact } from "@zocket/react"; import type { app } from "../server/app"; export const client = createClient({ url: "ws://localhost:3000", }); export const { ZocketProvider, useClient, useActor, useEvent, useActorState, useConnectionStatus, } = createZocketReact(); ``` ## Add the Provider [Section titled “Add the Provider”](#add-the-provider) Wrap your app with `ZocketProvider`: src/App.tsx ```tsx import { ZocketProvider, client } from "./zocket"; export function App() { return ( ); } ``` The provider makes the client available to all hooks via React Context. ## What You Get [Section titled “What You Get”](#what-you-get) | Export | Description | | :-------------------- | :---------------------------------------------- | | `ZocketProvider` | Context provider — wrap your app with it | | `useClient` | Get the typed client from context | | `useActor` | Get a ref-counted actor handle | | `useEvent` | Subscribe to actor events with auto-cleanup | | `useActorState` | Subscribe to actor state with optional selector | | `useConnectionStatus` | Returns the WebSocket connection status | All hooks are fully typed from your actor definitions — no manual type annotations needed. ## Multiple Apps [Section titled “Multiple Apps”](#multiple-apps) If you have multiple Zocket apps (e.g. different servers), create separate factories: game-zocket.ts ```ts export const { ZocketProvider: GameProvider, useActor: useGameActor } = createZocketReact(); // chat-zocket.ts export const { ZocketProvider: ChatProvider, useActor: useChatActor } = createZocketReact(); ``` Nest the providers in your app: ```tsx ``` # Roadmap > Upcoming features — targeted events, timers, cron, actor-to-actor calls, streaming, and AI SDK integration. Features that are designed and coming soon. The APIs shown here are stable — implementation is in progress. ## Targeted Events [Section titled “Targeted Events”](#targeted-events) Currently, `emit("event", payload)` broadcasts to all subscribers. Targeted events let you send to a specific client: ```ts const GameRoom = actor({ state: z.object({ players: z.record(z.object({ hand: z.array(z.string()), score: z.number(), })).default({}), }), methods: { dealCards: { handler: ({ state, emit, connections }) => { // Broadcast to everyone emit("roundStarted", { round: 1 }); // Send each player their private hand for (const id of connections.list()) { const player = state.players[id]; if (player) { emit.to(id, "yourHand", { cards: player.hand }); } } }, }, }, events: { roundStarted: z.object({ round: z.number() }), yourHand: z.object({ cards: z.array(z.string()) }), }, }); ``` **`emit.to(connectionId, event, payload)`** — send to one specific connection. **`connections.list()`** — returns `string[]` of connected client IDs. State stays broadcast (all subscribers see the same patches). Private data goes through targeted events. *** ## Timers [Section titled “Timers”](#timers) Actors can schedule delayed self-calls. A timer invokes a method on the same actor instance after a delay, through the same sequential queue. ```ts import { z } from "zod"; import { actor } from "@zocket/core"; const GameRoom = actor({ state: z.object({ phase: z.enum(["lobby", "countdown", "playing"]).default("lobby"), }), methods: { startCountdown: { handler: ({ state, timer }) => { state.phase = "countdown"; timer.after(10_000).beginRound(); }, }, beginRound: { handler: ({ state }) => { state.phase = "playing"; }, }, }, }); ``` `timer` is available in every handler context — method handlers, `onConnect`, `onDisconnect`, `onActivate`. ### API [Section titled “API”](#api) `timer.after(ms)` and `timer.every(ms)` return a typed proxy of the actor’s own methods. Calling a method on the proxy schedules it. ```ts timer.after(5000).beginRound(); // one-shot, autocompletes method names timer.every(1000).tick(); // recurring interval timer.after(5000).set({ value: 10 }); // input type-checked against schema const id = timer.every(1000).tick(); // returns cancellable ID timer.cancel(id); ``` Fully type-safe — method names autocomplete, inputs are checked against schemas, typos are caught at compile time. The same method chaining pattern as the client SDK. ### Queueing behavior [Section titled “Queueing behavior”](#queueing-behavior) Timer-invoked methods go through the same sequential queue as client-initiated calls. If a method is currently executing when a timer fires, the timer’s method call waits in the queue until the current method finishes. This preserves the single-writer guarantee — no concurrent execution, ever. All timers are cleared when an actor is deactivated or destroyed. *** ## Cron [Section titled “Cron”](#cron) Actors can declare recurring schedules as part of their definition. Auto-started on activation, auto-stopped on deactivation. ```ts const Presence = actor({ state: z.object({ users: z.record(z.number()).default({}), }), methods: { heartbeat: { handler: ({ state, connectionId }) => { state.users[connectionId] = Date.now(); }, }, cleanup: { handler: ({ state }) => { const now = Date.now(); for (const [id, lastSeen] of Object.entries(state.users)) { if (now - lastSeen > 30_000) delete state.users[id]; } }, }, }, cron: { cleanup: { every: 30_000 }, }, }); ``` Method names are type-checked against the actor’s defined methods at compile time. Only methods without required input can be used with cron — there’s no way to pass input to a cron-scheduled call. Cron is syntactic sugar — internally it creates intervals via the timer system during `onActivate`. You can achieve the same thing programmatically with `timer.every(30_000).cleanup()` inside `onActivate`. *** ## Actor-to-Actor Calls [Section titled “Actor-to-Actor Calls”](#actor-to-actor-calls) Actors can call methods on other actor instances from within a handler. ```ts const Lobby = actor({ state: z.object({ players: z.array(z.string()).default([]), }), methods: { startMatch: { handler: async ({ state, actors }) => { const matchId = crypto.randomUUID(); await actors.match(matchId).initialize({ players: state.players, }); state.players = []; return { matchId }; }, }, }, }); ``` `actors` provides a proxy with the same shape as the client SDK: `actors.actorType(id).method(input)`. ### Fire-and-forget [Section titled “Fire-and-forget”](#fire-and-forget) By default, actor-to-actor calls are request/response — the caller awaits the result. For background work and delegation, you can call without awaiting: ```ts handler: async ({ state, actors }) => { // Request/response — waits for result const result = await actors.researcher("topic").search({ query: "..." }); // Fire-and-forget — don't await, work happens in background actors.worker("abc").process({ data: state.items }); // no await — returns immediately, result ignored } ``` Fire-and-forget is important for agent delegation (tell 5 workers to start without blocking) and for avoiding deadlocks in circular actor communication. ### Typing [Section titled “Typing”](#typing) Actor-to-actor calls are not type-safe at the `actor()` definition level. This is a fundamental TypeScript limitation — `actor()` is called before `createApp()` assembles the registry, creating a circular type dependency that cannot be resolved. At runtime, the proxy is fully functional. Errors surface as runtime exceptions. This is the same trade-off other actor frameworks make (Erlang GenServers, Temporal activities). *** ## Streaming Methods [Section titled “Streaming Methods”](#streaming-methods) By default, state patches are computed and broadcast when a handler finishes. For long-running methods (LLM calls, file processing, multi-step workflows), you want patches to stream to clients as state changes. Mark a method as streaming with `stream: true`. The runtime automatically broadcasts state patches at a regular interval while the handler executes: ```ts methods: { // Regular method — patches sent on completion reset: { handler: ({ state }) => { state.output = ""; state.status = "idle"; }, }, // Streaming method — patches sent automatically as state changes generate: { stream: true, handler: async ({ state }) => { state.status = "thinking"; // client sees "thinking" within ~50ms for await (const chunk of llmStream) { state.output += chunk; // patches batch and send automatically every tick } state.status = "done"; }, }, }, ``` The handler looks exactly like a regular handler. No manual flush calls, no generators, no new syntax. `stream: true` is the only change. Under the hood, the runtime runs a tick loop (\~50ms) during streaming methods: finalize the Immer draft, compute JSON patches, broadcast to subscribers, create a fresh draft. When the handler finishes, a final flush sends remaining patches. ```ts // Configuration options stream: true, // default interval (~50ms) stream: { interval: 16 }, // ~60fps for game state stream: { interval: 100 }, // lower frequency for less chatty updates ``` *** ## Streaming RPC [Section titled “Streaming RPC”](#streaming-rpc) Separate from streaming methods (which stream state patches to all subscribers), Streaming RPC lets a method send partial **return values** to the specific caller. A new protocol message type `rpc:stream` sends chunks before the final `rpc:result`: ```plaintext → { type: "rpc", id: "rpc_1", actor: "agent", actorId: "run-1", method: "generate", input: { prompt: "..." } } ← { type: "rpc:stream", id: "rpc_1", chunk: "Hello" } ← { type: "rpc:stream", id: "rpc_1", chunk: " world" } ← { type: "rpc:result", id: "rpc_1", result: "Hello world" } ``` This is a general-purpose mechanism — useful for AI token streaming, file processing progress, or any method that produces incremental output. The AI SDK integration uses this to bridge `useChat()`’s streaming protocol over Zocket’s WebSocket transport. *** ## AI SDK Integration [Section titled “AI SDK Integration”](#ai-sdk-integration) Zocket integrates with the Vercel AI SDK and TanStack AI SDK. Developers keep their familiar `useChat()` hooks — the backend is a Zocket actor instead of an API route. ### Server [Section titled “Server”](#server) ```ts import { streamText } from "ai"; import { openai } from "@ai-sdk/openai"; import { actor } from "@zocket/core"; import { aiHandler } from "@zocket/ai"; const Conversation = actor({ state: z.object({ messages: z.array(z.any()).default([]), }), methods: { chat: aiHandler({ model: openai("gpt-4o"), }), }, }); ``` `aiHandler()` wraps the AI SDK’s `streamText()` into a Zocket actor method. It reads messages from actor state, calls the LLM, streams the response in the AI SDK’s wire format, and updates `state.messages` when complete. ### Client [Section titled “Client”](#client) ```tsx import { useChat } from "ai/react"; import { useZocketAI } from "@zocket/ai/react"; function Chat({ id }: { id: string }) { const { messages, input, handleSubmit, isLoading } = useChat({ fetch: useZocketAI("conversation", id), }); return (
{messages.map(m => )} setInput(e.target.value)} /> ); } ``` `useZocketAI()` returns a `fetch`-compatible adapter that translates `useChat()`’s HTTP requests into Zocket WebSocket messages. The AI SDK hooks work unchanged. ### What this gives you over plain `useChat()` [Section titled “What this gives you over plain useChat()”](#what-this-gives-you-over-plain-usechat) By putting `useChat()` on top of a Zocket actor instead of an API route: * **Multiplayer conversations** — open the same chat in two tabs, both see tokens stream. HTTP-based `useChat()` is per-client. * **Server-authoritative history** — the actor owns the messages. No client-side state to reconcile. * **Actor lifecycle** — timeouts, tool delegation to other actors, cron for periodic work. * **Survives reconnects** — the actor persists across WebSocket disconnections. ### Packages [Section titled “Packages”](#packages) | Package | What it does | | ------------------ | --------------------------------------------------------------------- | | `@zocket/ai` | Server — `aiHandler()` wraps AI SDK’s `streamText` into actor methods | | `@zocket/ai/react` | Client — `useZocketAI()` adapter for `useChat({ fetch })` | *** ## How They Compose [Section titled “How They Compose”](#how-they-compose) These features work together naturally. Here’s an AI agent that uses `stream: true` for token streaming, timers for timeout safety, and actor-to-actor calls for tool delegation: ```ts import { streamText } from "ai"; import { openai } from "@ai-sdk/openai"; import { actor } from "@zocket/core"; const AgentRun = actor({ state: z.object({ messages: z.array(z.any()).default([]), status: z.enum(["running", "waiting", "done"]).default("running"), }), methods: { start: { input: z.object({ prompt: z.string() }), stream: true, handler: async ({ state, input, timer, actors }) => { state.messages.push({ role: "user", content: input.prompt }); state.messages.push({ role: "assistant", content: "" }); const result = streamText({ model: openai("gpt-4o"), messages: state.messages.slice(0, -1), }); for await (const chunk of result.textStream) { state.messages.at(-1).content += chunk; // patches stream to client automatically } if (result.toolCalls?.length) { for (const tool of result.toolCalls) { await actors.tool(tool.name).execute(tool.args); } } timer.after(30_000).timeout(); state.status = "done"; }, }, timeout: { handler: ({ state }) => { if (state.status === "running") { state.status = "done"; } }, }, }, }); ``` `stream: true` for token streaming. Timer for timeout safety. Actor-to-actor for tool delegation. All running through the same sequential queue with single-writer guarantees. *** ## Type Safety [Section titled “Type Safety”](#type-safety) | Feature | Type-safe? | Details | | ------------------------------- | ---------- | ---------------------------------------------- | | `emit("event", payload)` | Yes | Event names and payloads type-checked | | `emit.to(id, "event", payload)` | Yes | Same, routed to specific connection | | `connections.list()` | Yes | Returns `string[]` | | `timer.after(ms).method()` | Yes | Method names autocomplete, inputs type-checked | | `timer.every(ms).method()` | Yes | Same | | `timer.cancel(id)` | Yes | | | `cron: { method: { every } }` | Yes | Method names constrained to `keyof TMethods` | | `stream: true` | Yes | Declarative, no handler changes | | `actors.type(id).method()` | No | Circular type dependency at definition time | # Bun Adapter > Serve your Zocket app with Bun in one line. The `@zocket/server` package ships with a first-class Bun adapter. ## Quick Start [Section titled “Quick Start”](#quick-start) ```ts import { serve } from "@zocket/server/bun"; import { app } from "./app"; const server = serve(app, { port: 3000 }); console.log(`Zocket on ws://localhost:${server.port}`); ``` That’s it. `serve()` creates a Bun HTTP server that upgrades WebSocket connections and wires up all the actor message handling. ## Options [Section titled “Options”](#options) ```ts serve(app, { port: 3000, // default: 0 (random available port) hostname: "0.0.0.0", }); ``` The return value is a standard `Bun.Server` instance. ## Custom Setup with `createBunHandlers` [Section titled “Custom Setup with createBunHandlers”](#custom-setup-with-createbunhandlers) For more control (e.g., adding HTTP routes alongside WebSocket), use `createBunHandlers`: ```ts import { createBunHandlers } from "@zocket/server/bun"; import { app } from "./app"; const zocket = createBunHandlers(app); Bun.serve({ port: 3000, fetch(req, server) { // Try WebSocket upgrade first const wsResponse = zocket.fetch(req, server); if (wsResponse === undefined) return wsResponse; // upgrade succeeded // Custom HTTP routes const url = new URL(req.url); if (url.pathname === "/health") { return new Response("ok"); } return new Response("Not Found", { status: 404 }); }, websocket: zocket.websocket, }); ``` ### BunHandlers Shape [Section titled “BunHandlers Shape”](#bunhandlers-shape) ```ts interface BunHandlers { fetch(req: Request, server: BunServer): Response | undefined; websocket: WebSocketHandler; } ``` * `fetch` — attempts to upgrade the request to WebSocket. Returns `undefined` on success, or a `Response` on failure. * `websocket` — Bun’s `WebSocketHandler` with `open`, `message`, and `close` callbacks wired to Zocket’s handler. ## How It Works [Section titled “How It Works”](#how-it-works) Under the hood: 1. `createBunHandlers(app)` calls `createHandlers(app)` to get runtime-agnostic callbacks 2. Each WebSocket connection gets a `BunConnectionAdapter` with a unique `id` 3. The adapter implements the `Connection` interface (`send()` + `id`) 4. Messages are routed through the handler to the `ActorManager` ## Connection IDs [Section titled “Connection IDs”](#connection-ids) Each connection gets a unique ID like `bun_1_m3abc`. This ID is: * Passed to method handlers as `connectionId` * Used by lifecycle hooks (`onConnect`, `onDisconnect`) * Stable for the lifetime of a WebSocket connection # Custom Handlers > Build custom server adapters with createHandlers(). If you’re not using Bun, or need full control over the WebSocket lifecycle, you can use `createHandlers()` to get runtime-agnostic callbacks. ## createHandlers [Section titled “createHandlers”](#createhandlers) ```ts import { createHandlers } from "@zocket/server"; import { app } from "./app"; const handlers = createHandlers(app); ``` This returns a `HandlerCallbacks` object: ```ts interface HandlerCallbacks { onConnection(conn: Connection): void; onMessage(conn: Connection, raw: string): Promise; onClose(conn: Connection): void; manager: ActorManager; } ``` Note that `onMessage` is async — if you need to know when processing is done (e.g., to ack a message queue delivery), await it. ## Connection Interface [Section titled “Connection Interface”](#connection-interface) Your adapter must provide objects that implement `Connection`: ```ts interface Connection { /** Send a serialized message down the transport. */ send(message: string): void; /** Stable identifier for lifecycle hooks. Assigned by the adapter. */ id: string; /** Authenticated user identity, when the transport provides it. */ userId?: string | null; /** Verified auth claims or other connection-scoped metadata. */ claims?: Record; /** Optional routing scope attached by the transport layer. */ scope?: Record; } ``` The `id` must be unique per connection and stable for its lifetime. The optional `userId`, `claims`, and `scope` fields flow through from the transport (e.g. the gateway’s JWT verification) into method `ctx` and actor lifecycle hooks, so actors can use them for authorization. ## Wiring to a Custom Runtime [Section titled “Wiring to a Custom Runtime”](#wiring-to-a-custom-runtime) Here’s a sketch for wiring to a generic WebSocket server: ```ts import { createHandlers } from "@zocket/server"; import { app } from "./app"; const handlers = createHandlers(app); let connId = 0; myWebSocketServer.on("connection", (ws) => { const conn = { id: `custom_${++connId}`, send: (msg: string) => ws.send(msg), }; handlers.onConnection(conn); ws.on("message", async (data: string) => { await handlers.onMessage(conn, data); }); ws.on("close", () => { handlers.onClose(conn); }); }); ``` ## Message Routing [Section titled “Message Routing”](#message-routing) The handler routes messages based on their `type` field: | Message Type | Action | | :------------ | :----------------------------------------- | | `rpc` | Invoke method, send `rpc:result` back | | `event:sub` | Subscribe connection to actor events | | `event:unsub` | Unsubscribe from events | | `state:sub` | Subscribe to state + send initial snapshot | | `state:unsub` | Unsubscribe from state patches | ## ActorManager [Section titled “ActorManager”](#actormanager) The `manager` property on `HandlerCallbacks` gives you direct access to the actor manager. You can also import and construct one yourself: ```ts import { ActorManager } from "@zocket/server"; ``` ### Inspecting State [Section titled “Inspecting State”](#inspecting-state) ```ts const { manager } = handlers; // Number of hot actor instances console.log(manager.size); // List all hot actors const actors = manager.list(); // [{ actorName: "counter", actorId: "main" }, ...] ``` ### Destroying Actors [Section titled “Destroying Actors”](#destroying-actors) ```ts // Destroy a specific actor (calls onDeactivate if defined) await manager.destroy("counter", "main"); // Destroy all actors (for shutdown or redeploy) await manager.destroyAll(); ``` ### Lifecycle Events [Section titled “Lifecycle Events”](#lifecycle-events) Subscribe to actor creation and destruction: ```ts manager.on("actorCreated", ({ actorName, actorId }) => { console.log(`Actor created: ${actorName}/${actorId}`); }); manager.on("actorDestroyed", ({ actorName, actorId }) => { console.log(`Actor destroyed: ${actorName}/${actorId}`); }); ``` The `on()` method returns an unsubscribe function. ## State Initialization [Section titled “State Initialization”](#state-initialization) When an actor instance is first created, the manager initializes state by: 1. Validating `{}` against the state schema (works with schemas that have defaults) 2. If that fails, validating `undefined` (works with top-level `.default()`) 3. If both fail, using `{}` as a fallback After initialization, the actor’s `onActivate` hook is called if defined. # Deno Adapter > Serve your Zocket app with Deno. The Deno adapter is currently in development. It will provide first-class support for serving Zocket apps on Deno using the built-in `Deno.serve` API. Check back soon or follow progress on [GitHub](https://github.com/ChiChuRita/zocket). # Node Adapter > Serve your Zocket app with Node.js. The Node.js adapter is currently in development. It will provide first-class support for serving Zocket apps on Node.js using the `ws` library. Check back soon or follow progress on [GitHub](https://github.com/ChiChuRita/zocket). # Use Cases > The kinds of products Zocket is built for. Zocket is strongest when your app is made of live entities clients interact with directly. If you naturally think in terms of actors with identity, behavior, state, and subscriptions, Zocket is probably a good fit. ## Chat And Messaging [Section titled “Chat And Messaging”](#chat-and-messaging) Chat threads, support conversations, group channels, and DMs all map naturally to actors. Each thread has: * an identity * a message history or derived state * methods like `sendMessage`, `markRead`, or `rename` * clients subscribed to updates in realtime This is where Zocket feels more natural than stitching together REST mutations, websocket events, and client-side caches by hand. ## AI Agents [Section titled “AI Agents”](#ai-agents) Agent sessions and threads are a strong fit for Zocket’s model. An agent is not just a stream of tokens. It usually has: * an identity * mutable session state * tools or methods it can invoke * subscribers watching progress in realtime That makes the actor model a better fit than a flat API plus a side channel for live updates. ## Realtime Workflows [Section titled “Realtime Workflows”](#realtime-workflows) Some workflows behave like long-lived entities: * order fulfillment sessions * onboarding flows * approval chains * background jobs with live progress When users need to see status, invoke actions, and stay synced to a running process, modeling that workflow as an actor keeps the logic and the state in one place. ## Multiplayer And Rooms [Section titled “Multiplayer And Rooms”](#multiplayer-and-rooms) Rooms, lobbies, matches, and shared game state are classic actor-shaped problems. They have: * sequential actions * shared mutable state * multiple clients connected at once * a clear unit of coordination This is one of the most direct fits for Zocket. ## Collaborative State [Section titled “Collaborative State”](#collaborative-state) Some products revolve around shared live objects: * documents * whiteboards * boards * sessions * shared controls If your frontend needs a typed handle to a live object and a clean way to subscribe to its changing state, Zocket gives that object a direct runtime shape. ## When Zocket Is Probably Not The Right Tool [Section titled “When Zocket Is Probably Not The Right Tool”](#when-zocket-is-probably-not-the-right-tool) Zocket is a weaker fit when: * your app is mostly request/response CRUD * realtime is only occasional notifications * you mainly need message transport, not stateful server-side entities * your strongest abstraction is database rows or pub/sub channels rather than live actors In those cases, tools centered on REST, reactive queries, or pub/sub may be simpler. ## The Short Rule [Section titled “The Short Rule”](#the-short-rule) If the core units of your product sound like chats, agents, sessions, rooms, workflows, documents, or matches, you are probably already thinking in Zocket’s model.