Serverless
realtime backends,
fully typed.
Use your schema library of choice, connect from typed clients, and keep realtime state in sync without socket boilerplate.
import { actor, createApp, serve } from "@zocket/core";
import { z } from "zod";
const ChatRoom = actor({
state: z.object({
messages: z.array(z.object({
from: z.string(), text: z.string(),
})).default([]),
}),
methods: {
send: {
input: z.object({ from: z.string(), text: z.string() }),
handler: ({ state, input }) => { state.messages.push(input) },
},
},
});
export const app = createApp({ actors: { chat: ChatRoom } });
serve(app, { port: 3000 }); 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.send({ from: "alice", text: "hello!" });
room.state.subscribe((s) => {
console.log(s.messages);
// → [{ from: "alice", text: "hello!" }]
}); Three primitives. That's the API.
Zocket exposes an actor model, not a socket library. You describe state, describe methods, and read state on the client. The runtime handles the rest.
Actors own state.
Describe state with Zod. Describe methods with typed inputs. No classes, no decorators, no codegen.
const Room = actor({
state: z.object({
messages: z.array(z.string()).default([]),
}),
methods: {
send: {
input: z.string(),
handler: ({ state, input }) => {
state.messages.push(input);
},
},
},
});Clients call methods.
Full types flow from server to client. No REST, no OpenAPI, no tRPC boilerplate. Just import the app type.
const room = client.room("general");
await room.send("hello");
// ^ typed. typos fail at compile time.State streams back.
You subscribe to state, not events. Immer patches flow over WebSocket. React hooks included.
room.state.subscribe((s) => {
console.log(s.messages);
});
// or in React:
const s = useActorState(room);Everything realtime apps need, in one runtime.
No separate database service, no queue, no socket server, no scheduler to manage.
State next to compute
State lives in memory where your code runs. Reads and writes take microseconds, not network round-trips.
Live state on the client
Clients always see the latest state. Skip the fetch-cache-revalidate dance entirely.
WebSockets out of the box
Push-based and bidirectional. The right transport for anything that actually changes.
Workflows, queues, scheduling
Long-running jobs, durable queues, and timers. One runtime instead of three services.
No lost requests
Calls survive disconnects. Clients reconnect and the answer is waiting for them.
Scales to zero, scales to anything
Idle actors cost nothing. Traffic spikes get absorbed. No capacity planning, no idle servers.
Same feature, very different runtime model.
Compare the amount of ceremony, infrastructure assumptions, and state management you carry into each use case.
A chat room with message history and online presence.
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, clientId }) {
const i = state.online.indexOf(clientId);
if (i !== -1) state.online.splice(i, 1);
},
});How it runs
Start with one process. Scale to a distributed cluster when you need it.
Standalone
One process, zero infra. For prototypes and small apps.
Distributed
Gateway + NATS + runtime. Self-hosted, scales independently.
Platform
Multi-tenant, Firecracker isolation, fully hosted.
Coming soon
These features are designed and in progress. Read the roadmap →