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.

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.