🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

@firtoz/socka

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@firtoz/socka

Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects

latest
Source
npmnpm
Version
3.0.3
Version published
Weekly downloads
223
-53.25%
Maintainers
1
Weekly downloads
 
Created
Source

@firtoz/socka

npm version npm downloads license

TypeScript WebSocket Standard Schema

Socka — WebSocket RPC, Standard Schema

Typed WebSocket RPC and pushes for TypeScript. One defineSocka contract gives you session.send.* for RPCs and session.subscribe for typed server pushes—validated, correlated, same schema on client and server.

Validation is Standard Schema v1, not Zod-specific: any compliant library works (e.g. Zod, Valibot, or others). Examples below use Zod for familiarity.

npm: @firtoz/socka. Socka is the project name in prose; install and import paths always use @firtoz/socka or @firtoz/socka/.... The published artifact is compiled ESM + .d.ts in dist/ (see package.json exports).

Call output shapes (at a glance)

GoalIn defineSocka calls
Fire-and-forget (cursors, live drafts, high frequency)Omit output
Request/response await after the handler runsNormal output schema
Correlated ack with no payloadoutput: z.void()

Details: Client — Fire-and-forget · Reference — Optional output. For output-less calls, void send.foo(...).catch(...) does not observe serverError—use reportError on SockaSession / useSockaSession (see Client — Fire-and-forget observability).

React + Cloudflare Durable ObjectsReact + Durable Objects (shared contract, SockaWebSocketDO, useSockaSession, no casts). Canvas / whiteboard-style contract sketch: Collaborative realtime.

Hand-written types next to Zod under exactOptionalPropertyTypes can break inference—see Reference — TypeScript and exact optional properties.

Minimal example: multi-room chat (Bun)

Join/leave and live messages use pushes; persisted lines use listHistory; who is connected uses listPresence; clearHistory wipes stored messages and historyCleared notifies the room (examples also show presence in the UI). This snippet keeps history in memory so it stays short—see chatroom-bun for SQLite, chatroom-hono for file JSON, and chatroom-do for Durable Object SQLite.

contract.ts (shared):

import { defineSocka } from "@firtoz/socka/core";
import * as z from "zod";

export const messageRow = z.object({
	id: z.string(),
	ts: z.number(),
	userId: z.string(),
	displayName: z.string(),
	text: z.string(),
});

export type ChatMessageRow = z.infer<typeof messageRow>;

const onlineUser = z.object({
	userId: z.string(),
	displayName: z.string(),
});

export const chatContract = defineSocka({
	calls: {
		listHistory: {
			input: z.object({ limit: z.number().int().min(1).max(500).optional() }),
			output: z.object({ messages: z.array(messageRow) }),
		},
		listPresence: {
			input: z.object({}).optional(),
			output: z.object({
				selfUserId: z.string(),
				users: z.array(onlineUser),
			}),
		},
		sendMessage: {
			input: z.object({ text: z.string().min(1) }),
			output: z.object({ ok: z.literal(true) }),
		},
		clearHistory: {
			input: z.object({}).optional(),
			output: z.object({ ok: z.literal(true) }),
		},
	},
	pushes: {
		userJoined: z.object({ userId: z.string(), displayName: z.string() }),
		userLeft: z.object({
			userId: z.string(),
			displayName: z.string(),
		}),
		roomMessage: messageRow,
		historyCleared: z.object({
			ts: z.number(),
			clearedByUserId: z.string(),
			clearedByDisplayName: z.string(),
		}),
	},
});

Fire-and-forget vs output: z.void() — Omit output on a call when you want one-way success semantics: the server does not send a serverResponse, and await session.send.* resolves after the frame is sent (it does not wait for server processing). Server failures still return a correlated serverError; use reportError on SockaSession / useSockaSession to observe those when using output-less calls. Use output: z.void() when you still want a normal request/response await that completes only after the server acknowledges.

server.tscreateSockaRoomRegistry gives each room its own sessionMap and config. By default createData receives SockaStrictWebSocketInit: init.request is the upgrade Request (Bun/Hono/adapters pass it through; see Server — Strict upgrade request). Set strictUpgradeRequest: false when you have no Request. session.listPeers() returns session.data for other sockets in the same room—use it to implement listPresence-style calls (see Presence).

import type { ServerWebSocket } from "bun";
import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
import {
	createSockaRoomRegistry,
	type SockaWebSocketSessionConfig,
} from "@firtoz/socka/server";
import { type ChatMessageRow, chatContract } from "./contract";

type SessionData = { roomId: string; userId: string; displayName: string };

/** In-memory demo store — swap for SQLite / files / DO in real apps. */
const history = new Map<string, ChatMessageRow[]>();

const registry = createSockaRoomRegistry(
	(roomId): SockaWebSocketSessionConfig<typeof chatContract, SessionData> => ({
		contract: chatContract,
		createData: (init) => {
			const u = new URL(init.request.url);
			const displayName = u.searchParams.get("name")?.trim() || "anon";
			return { roomId, userId: crypto.randomUUID(), displayName };
		},
		onAttached: async (session) => {
			await session.broadcastPush(
				"userJoined",
				{ userId: session.data.userId, displayName: session.data.displayName },
				true,
			);
		},
		handlers: {
			listHistory: async (input, session) => {
				const lim = input.limit ?? 200;
				const rows = history.get(session.data.roomId) ?? [];
				return { messages: rows.slice(-lim) };
			},
			listPresence: async (_input, session) => {
				const users = session.listPeers().map((d) => ({
					userId: d.userId,
					displayName: d.displayName,
				}));
				users.sort((a, b) => a.displayName.localeCompare(b.displayName));
				return { selfUserId: session.data.userId, users };
			},
			sendMessage: async (input, session) => {
				const row = {
					id: crypto.randomUUID(),
					ts: Date.now(),
					userId: session.data.userId,
					displayName: session.data.displayName,
					text: input.text,
				};
				const list = history.get(session.data.roomId) ?? [];
				list.push(row);
				history.set(session.data.roomId, list);
				await session.broadcastPush("roomMessage", row);
				return { ok: true as const };
			},
			clearHistory: async (_input, session) => {
				history.set(session.data.roomId, []);
				const ts = Date.now();
				await session.broadcastPush("historyCleared", {
					ts,
					clearedByUserId: session.data.userId,
					clearedByDisplayName: session.data.displayName,
				});
				return { ok: true as const };
			},
		},
		handleClose: async (session) => {
			await session.broadcastPush(
				"userLeft",
				{ userId: session.data.userId, displayName: session.data.displayName },
				true,
			);
		},
	}),
);

type BunWsData = { roomId: string; request: Request };

const { websocket } = createSockaBunWebSocketHandlers({
	resolveScope(ws: ServerWebSocket<BunWsData>) {
		const { roomId } = ws.data;
		const room = registry.get(roomId);
		return { sessionMap: room.sessionMap, config: room.config };
	},
});

Bun.serve<BunWsData>({
	port: 3450,
	fetch(req, server) {
		const url = new URL(req.url);
		if (url.pathname.startsWith("/ws/")) {
			const roomId = decodeURIComponent(url.pathname.slice(4)) || "default";
			if (server.upgrade(req, { data: { roomId, request: req } })) return undefined;
			return new Response("WebSocket upgrade failed", { status: 400 });
		}
		return new Response("OK");
	},
	websocket,
});

Ports: this minimal snippet listens on 3450; the full-stack examples use 3461–3466.

client.ts (browser or Bun):

import { SockaSession } from "@firtoz/socka/client";
import { chatContract } from "./contract";

const session = new SockaSession({
	contract: chatContract,
	url: "ws://localhost:3450/ws/lobby?name=Ada",
});

session.subscribe.on("userJoined", (p) => console.log("joined", p));
session.subscribe.on("userLeft", (p) => console.log("left", p.displayName));
session.subscribe.on("roomMessage", (m) => console.log(`${m.displayName}: ${m.text}`));
session.subscribe.on("historyCleared", (p) =>
	console.log("history cleared by", p.clearedByDisplayName, "at", p.ts),
);

const { messages } = await session.send.listHistory({});
console.log("history", messages);
const { selfUserId, users } = await session.send.listPresence({});
console.log("online", selfUserId, users);
await session.send.sendMessage({ text: "hello room" });
await session.send.clearHistory({});

Run bun run server.ts, then point the client at the same ws://…/ws/<room>?name=… path you upgrade in fetch.

More examples: chatroom-bun (SQLite + multi-room UI) · chatroom-hono · chatroom-do · tic-tac-toe Bun · Hono + Node · Cloudflare DO.

Install

Always install @firtoz/socka, then add only what your imports need (npm / pnpm / bun add as you prefer):

You are building…Install
Browser / Vite SPA (client only)npm install @firtoz/socka
React (@firtoz/socka/react)npm install @firtoz/socka react — add @types/react as a dev dependency if TypeScript asks
Bun (Bun.serve, @firtoz/socka/bun)npm install @firtoz/socka — add bun-types as a dev dependency if you type-check Bun APIs
Node + Hono + @hono/node-wsnpm install @firtoz/socka hono @hono/node-ws @hono/node-server ws — add @types/ws as a dev dependency when you use ws on Node
Cloudflare Workers + Hono (@firtoz/socka/hono/cloudflare)npm install @firtoz/socka hono
Cloudflare Durable Objects (@firtoz/socka/do)npm install @firtoz/socka hono @firtoz/websocket-do

For Cloudflare TypeScript types, prefer wrangler types (or your app’s typegen) so globals and bindings match your Worker — see Cloudflare’s TypeScript guide. More detail: Peers.

Other runtimes

Pick how the socket is upgraded, then use the matching import path and guide:

RuntimeImport pathQuick start
Node + ws (or any standard WebSocket after upgrade)@firtoz/socka/serverattachSockaWebSocket
Bun (Bun.serve / ServerWebSocket)@firtoz/socka/buncreateSockaBunWebSocketHandlers
Hono on Node (@hono/node-ws)@firtoz/socka/honosockaHonoNodeWs
Hono on Cloudflare Workers@firtoz/socka/hono/cloudflaresockaHonoCloudflare
Cloudflare Durable Objects@firtoz/socka/doDurable Objects

Why not socket.io, tRPC, or DIY?

  • Schema-first RPC + push — one contract; no parallel “event” protocol for server pushes.
  • Correlated envelopes — request/response IDs and validation hooks are built in.
  • Same contract across Bun, Hono, Node ws, and Durable Objects (see Comparison for socket.io, tRPC, and custom WebSocket stacks).
  • Room registry + presence helperscreateSockaRoomRegistry for per-room sessionMap / config; session.listPeers() to list other peers in the room (see Presence).
  • Strict upgrade typing + optional reconnect — by default createData sees init.request on the upgrade; SockaWebSocketClient / SockaSession can reconnect with exponential backoff (see Reconnection).

Documentation

Hub: docs/README.md (getting started, peers, lifecycle, multi-room, reference). React + Cloudflare DO: docs/react-durable-objects.md · Collaborative / canvas contracts: docs/collaborative-realtime.md.

Roadmap: deferred ideas and future work. Agent skills: skills/.

Full-stack examples

TopicStackFolderPort
Chat + historyBun + SQLitechatroom-bun3464
Chat + historyHono + Node + JSON fileschatroom-hono3465
Chat + historyCloudflare DO + Drizzle SQLitechatroom-do3466
Tic-tac-toeBuntic-tac-toe-bun3461
Tic-tac-toeHono + Nodetic-tac-toe-hono3462
Tic-tac-toeCloudflare DOtic-tac-toe-do3463

Chat apps: bun run dev (or wrangler dev for chatroom-do). Tic-tac-toe: same.

Keywords

websocket

FAQs

Package last updated on 26 Apr 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts