realtime serverless
for chats, agents, and rooms

Build chats, agents, sessions, workflows, and collaborative objects on a typed realtime runtime that keeps live state, method calls, and subscriptions in sync. Zocket gives you a cleaner serverless model for the realtime part of your app instead of stitching together REST, sockets, and client-side sync.

Define the live backend once, call typed methods from clients, and subscribe to state changes without building three separate systems just to ship one realtime feature.

$ bun add @zocket/core @zocket/client zod
see the fit

one runtime, many realtime products

The same serverless DX works across threads, agents, rooms, workflows, and collaborative state. If a part of your product needs to stay live over time, Zocket gives it a direct runtime shape.

Scroll through realtime serverless patterns

Swipe or scroll the cases below. The detail view follows the example you are centered on, so you can browse the kinds of live backend workflows Zocket is good at without leaving the page.

01/04
Support

Support threads

Treat each customer thread as a live serverless unit with methods, state, and subscriptions instead of bouncing between endpoints, socket events, and UI caches.

typed methodslive stateserverless fit
support-thread.ts
const thread = client.support("acme-42");
await thread.reply({ text: "Where is my refund?" });
const messages = useActorState(thread, (s) => s.messages);
const status = useActorState(thread, (s) => s.status);

why it feels good

  • One handle per thread, not three layers of glue.
  • Reply and subscribe from the same object.
  • Selectors keep the UI focused on the state slice it actually needs.

runtime handles

status, message history, thread actions, and subscriber updates

one definition

the wire is not a second API

Define the actor on the server, then let the client inherit its shape. Zocket uses Zod schemas to infer and validate methods, state, and subscriptions, so you stop maintaining a second contract just to talk to the live part of your product.

server.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 } });
inferred + validated
client.ts
import { createClient } from "@zocket/client";
import type { app } from "./server";

const client = createClient<typeof app>({
  url: "ws://localhost:3000",
});

const room = client.chat("general");

await room.sendMessage({ from: "Alice", text: "Hi!" });

console.log(room.state.messages); // fully typed

build the live part of your product

Zocket is strongest when your app needs a realtime serverless layer that stays live between calls. The runtime keeps policy, method boundaries, and state sync attached to the part of the product that owns them.

policy lives here

Put policy next to the live runtime

Auth, permissions, identity, and request context belong beside the live thing they govern. Zocket lets you gate runtime access and carry typed context into every method without scattering checks across routes and socket handlers.

middleware.ts
import { middleware } from "@zocket/core";
import { z } from "zod";
import { validateToken } from "./auth";

const authed = middleware()
  .use(async ({ connectionId }) => {
    const user = await validateToken(connectionId);
    if (!user) throw new Error("Unauthorized");
    return { userId: user.id, workspaceId: user.workspaceId };
  });

const SupportThread = authed.actor({
  state: z.object({
    messages: z.array(z.string()).default([]),
  }),
  methods: {
    reply: {
      input: z.object({ text: z.string() }),
      handler: ({ state, input, ctx }) => {
        state.messages.push(ctx.userId + ": " + input.text);
      },
    },
  },
});
safe live calls

Make live methods safe to call

Realtime methods should have real boundaries. Zod validates inputs at runtime, the client inherits the method shape automatically, and server-side failures come back as predictable errors instead of mysterious socket drama.

server.ts
const AgentSession = actor({
  state: z.object({
    status: z.enum(["active", "closed"]).default("active"),
    messages: z.array(z.string()).default([]),
  }),
  methods: {
    sendUserMessage: {
      input: z.object({ text: z.string().min(1) }),
      handler: ({ state, input }) => {
        if (state.status === "closed")
          throw new Error("Session is closed.");
        state.messages.push(input.text);
      },
    },
  },
});
client.ts
const session = client.agent("customer-123");

try {
  await session.sendUserMessage({ text: "refund status?" });
} catch (err) {
  console.log(err.message); // "Session is closed."
}
stay in sync

Keep clients attached to the right state

Rooms, threads, and sessions are only useful if the client can stay current without custom rebroadcast logic. Subscribe to the live runtime, select the slice you care about, and let the runtime handle the boring sync machinery.

state.ts
// React: subscribe to the slices you actually render
const messages = useActorState(thread, (s) => s.messages);
const status = useActorState(thread, (s) => s.status);

// Or subscribe to the whole live actor snapshot
const thread = client.support("thread-42");
thread.state.subscribe((state) => {
  console.log(state.messages, state.status);
});

See the difference

Same feature, different tools. Click a tab to compare.

Use case:Chat RoomA chat room with message history and online presence.
server.tsSocket.io
const io = new Server(3000);
const rooms = new Map();

io.on("connection", (socket) => {
  let room = null, name = null;

  socket.on("join", ({ room: r, name: n }) => {
    room = r; name = n;
    socket.join(r);
    const state = getRoom(r);
    state.online.add(n);
    socket.emit("history", state.messages);
    io.to(r).emit("presence", [...state.online]);
  });

  socket.on("sendMessage", ({ text }) => {
    if (!room || !name) return;
    const msg = { from: name, text };
    getRoom(room).messages.push(msg);
    io.to(room).emit("newMessage", msg);
  });

  socket.on("disconnect", () => {
    if (!room || !name) return;
    getRoom(room).online.delete(name);
    io.to(room).emit("presence", [...]);
  });
});
chat.tsZocket
const ChatRoom = actor({
  state: z.object({
    messages: z.array(z.object({
      from: z.string(),
      text: z.string(),
    })).default([]),
    online: z.array(z.string()).default([]),
  }),

  methods: {
    join: {
      input: z.object({ name: z.string() }),
      handler: ({ state, input }) => {
        state.online.push(input.name);
      },
    },
    sendMessage: {
      input: z.object({ from: z.string(), text: z.string() }),
      handler: ({ state, input }) => {
        state.messages.push(input);
      },
    },
  },

  onDisconnect({ state, connectionId }) {
    const i = state.online.indexOf(connectionId);
    if (i !== -1) state.online.splice(i, 1);
  },
});

No manual broadcasting, no room management, no untyped events. State syncs automatically.

ready to build?

Start with one live actor, run it locally, and hand the client a typed handle. The rest of the product can grow from there.

1

Install

Bring in the runtime pieces you need for actors, server, and client.

terminal
bun add @zocket/core @zocket/server @zocket/client zod
2

Define a live actor

Put the entity where your product actually lives in one place: state, methods, and the rules around it.

server.ts
import { actor, createApp } from "@zocket/core";
import { z } from "zod";

const SupportThread = actor({
  state: z.object({
    status: z.enum(["open", "closed"]).default("open"),
    messages: z.array(z.string()).default([]),
  }),
  methods: {
    reply: {
      input: z.object({ text: z.string() }),
      handler: ({ state, input }) => {
        state.messages.push(input.text);
      },
    },
  },
});

export const app = createApp({ actors: { support: SupportThread } });
3

Connect a typed client handle

Import the server type, connect to a specific actor instance, call methods, and subscribe to the state it owns.

client.ts
import { createClient } from "@zocket/client";
import type { app } from "./server";

const client = createClient<typeof app>({ url: "ws://localhost:3000" });
const thread = client.support("thread-42");

await thread.reply({ text: "Can you resend the invoice?" });
thread.state.subscribe((state) => console.log(state.messages));

questions before you switch models?

These are the things teams usually want to understand before replacing socket plumbing with actors.