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.
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.
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.
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
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.
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 } }); 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.
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.
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);
},
},
},
}); 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.
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);
},
},
},
}); const session = client.agent("customer-123");
try {
await session.sendUserMessage({ text: "refund status?" });
} catch (err) {
console.log(err.message); // "Session is closed."
} 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.
// 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.
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", [...]);
});
});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.
Install
Bring in the runtime pieces you need for actors, server, and client.
bun add @zocket/core @zocket/server @zocket/client zod Define a live actor
Put the entity where your product actually lives in one place: state, methods, and the rules around it.
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 } }); Connect a typed client handle
Import the server type, connect to a specific actor instance, call methods, and subscribe to the state it owns.
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));