typed actors
for realtime apps
Define stateful actors with typed methods, events, and state — then call them from any client with full end-to-end type safety over WebSockets.
$ 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.
🧦