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.

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 } });
types flow
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

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.

middleware.ts
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.

server.ts
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);
      },
    },
  },
});
client.ts
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.

state.ts
// 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

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

Define your actors

State, methods, events — all type-safe with Zod schemas.

server.ts
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.

client.ts
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.