typed realtime actors
for live products
Build chats, agents, rooms, sessions, and collaborative objects as stateful actors with identity, methods, state, and subscriptions. Zocket gives you the right abstraction for realtime apps, not just websocket plumbing.
$ bun add @zocket/core @zocket/client zod
end-to-end typesafety
With Zocket, your actor state, methods, and events are fully typed from server to client using Zod schemas. Never worry about sending the wrong data again.
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 } }); types flow
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 realtime on steroids
Batteries included. Type-safe by default. Zero tolerance for "it works on my machine."
the bouncer
Middleware
Your rooms have a velvet rope now. Auth, logging, rate limiting — fully type-safe, context flowing through like it owns the place. No ticket? No entry.
import { middleware, actor } from "@zocket/core";
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, role: user.role };
});
const PrivateRoom = authed.actor({
state: z.object({ messages: z.array(z.string()).default([]) }),
methods: {
send: {
input: z.object({ text: z.string() }),
handler: ({ state, input, ctx }) => {
state.messages.push(ctx.userId + ": " + input.text);
},
},
},
}); trust issues (the good kind)
Validation & Errors
Zod validates every input at runtime so your users can't send
{ name: undefined }
and gaslight your server into thinking that's fine.
Throw on server, catch on client.
Clean breakups only.
const GameRoom = actor({
state: z.object({
players: z.array(z.string()).default([]),
}),
methods: {
join: {
input: z.object({ name: z.string() }),
handler: ({ state, input }) => {
if (state.players.length >= 4)
throw new Error("Room is full!");
state.players.push(input.name);
},
},
},
}); const room = client.game("room-1");
try {
await room.join({ name: "Alice" });
} catch (err) {
console.log(err.message); // "Room is full!"
} selective hearing
State Subscriptions
Components only re-render when their slice of state changes. Your React tree finally learned to mind its own business.
// React: selector-based subscriptions
const messages = useActorState(room, (s) => s.messages);
const online = useActorState(room, (s) => s.online);
// Subscribe to full actor state
const room = client.chat("general");
room.state.subscribe((state) => {
console.log(state.messages);
}); ready to build?
Three steps. Zero config. Full type safety.
1
Install
bun add @zocket/core @zocket/server @zocket/client zod 2
Define your actors
State, methods, events — all type-safe with Zod schemas.
import { actor } from "@zocket/core";
import { z } from "zod";
const Counter = actor({
state: z.object({ count: z.number().default(0) }),
methods: {
increment: {
handler: ({ state }) => { state.count++ },
},
},
});
export const app = createApp({ actors: { counter: Counter } }); 3
Connect & ship
Import the type, create a client, call methods. That's it.
import { createClient } from "@zocket/client";
import type { app } from "./server";
const client = createClient<typeof app>({ url: "ws://localhost:3000" });
const counter = client.counter("my-counter");
await counter.increment();
// ✨ fully typed, fully realtime you have questions
We have answers. Probably.
🧦