Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

createSocketHub

Creates the Socket hub for a custom host, authorizing handshakes, accepting connections, validating events and distributing broadcasts across rooms and instances.

advanced API Since 1.0.0 @meekojs/meeko/server Source code
ts
import { createSocketHub } from "@meekojs/meeko/server";

createSocketHub is Meeko's transport-independent Socket core. It receives channel definitions (defineSocketChannel), the session manager and services, and validates everything coming in and going out: Origin, session and channel authorization at handshake, and the event and its payload against schemas on every message. It applies per-peer size, buffer and queue limits, and distributes broadcasts through a broker (LocalSocketPubSub on one instance, RedisSocketPubSub or Durable Objects on multiple instances).

The hub does not open sockets: the host performs the HTTP upgrade with its runtime and passes each connected peer to it. createSocketUpgradeHandler provides this bridge portably; createServerApp creates the hub automatically when the application has channels (app.socket.hub).

Usage#

ts
import { createSocketHub, defineSocketChannel, MemorySessionStore, SessionManager } from "@meekojs/meeko/server";
import type { Schema, SocketPeer } from "@meekojs/meeko/server";

const text: Schema<{ text: string }> = {
  "~standard": {
    version: 1,
    validate: (value) => typeof value === "object" && value !== null && typeof (value as { text?: unknown }).text === "string"
      ? { value: value as { text: string } }
      : { issues: [{ message: "Expected { text }" }] },
  },
};
const chat = defineSocketChannel({
  clientToServer: { message: text },
  serverToClient: { message: text },
  handlers: { message: ({ payload, emit }) => emit("message", { text: payload.text.toUpperCase() }) },
});

const hub = createSocketHub({ channels: { chat }, sessions: new SessionManager({ store: new MemorySessionStore() }), services: {} });

const sent: string[] = [];
const peer: SocketPeer = { send: (message) => { sent.push(message); }, close: () => {} };
const request = new Request("http://localhost/_meeko/socket/chat?room=general", { headers: { origin: "http://localhost" } });
const connection = await hub.accept("chat", request, peer, "general");
await connection.receive(JSON.stringify({ event: "message", payload: { text: "hello" } }));
console.log(sent); // [ '{"event":"message","payload":{"text":"HELLO"}}' ]

Type#

ts
function createSocketHub<TServices>(options: SocketHubOptions<TServices>): SocketHub

SocketHubOptions

ts
interface SocketHubOptions<TServices = unknown> {
	channels: Readonly<Record<string, unknown>>
	sessions: SessionManager
	services: TServices | ((request: Request) => TServices | Promise<TServices>)
	allowedOrigins?: readonly string[]
	maxMessageBytes?: number
	maxBufferBytes?: number
	maxPendingDeliveries?: number
	sessionRefreshMs?: number
	pubsub?: SocketPubSub
	platform?: ServerPlatform
	onDeliveryError?(error: unknown, context: SocketDeliveryErrorContext): void
}

SocketHub

ts
interface SocketHub {
  preflight(name: string, request: Request, roomId?: string): Promise<void>;
  accept(
    name: string,
    request: Request,
    peer: SocketPeer,
    roomId?: string,
  ): Promise<SocketConnection>;
  broadcast(name: string, roomId: string, event: string, payload: unknown): Promise<void>;
}

SocketPeer

ts
interface SocketPeer {
	send(message: string): void | Promise<void>
	close(code?: number, reason?: string): void
	readonly bufferedAmount?: number
}

SocketConnection

ts
interface SocketConnection {
	receive(message: string | Uint8Array): Promise<void>
	emit(event: string, payload: unknown): Promise<void>
	close(code?: number, reason?: string): void
}

Options#

Option Type Default Description
channels Record<string, SocketChannelDefinition> required Channels by name: the default export of each server/socket/<nombre>.ts.
sessions SessionManager required Loads the handshake session and closes the peer if revoked.
services S or (request) => S required Channel callback services, shared or created on each handshake.
allowedOrigins string[] none Accepted origins in addition to the handshake URL's own origin.
maxMessageBytes number 65 536 Largest incoming or outgoing serialized message. A larger incoming message closes with 1009; a larger broadcast rejects with SOCKET_LIMIT_EXCEEDED (413).
maxBufferBytes number 1 MiB Pending outgoing bytes per peer before closing it.
maxPendingDeliveries number 256 Pending broker deliveries per peer; a slower peer closes with 1013.
sessionRefreshMs number 1000 How long deliveries reuse the last session read; 0 rereads on every delivery.
pubsub SocketPubSub LocalSocketPubSub Cross-instance broker.
platform ServerPlatform none What callbacks see in context.platform.
onDeliveryError (error, { channel, room, event? }) => void console.error Failed deliveries to a local peer, which is closed. Never reaches the sender.

Return value#

A SocketHub:

Method Description
preflight(name, request, roomId?) Checks the channel, Origin, session and authorization before upgrading. Rejects with ApiError: UNKNOWN_SOCKET_CHANNEL (404), INVALID_ORIGIN (403), channel 401 or 403.
accept(name, request, peer, roomId?) Accepts an already-connected peer; resolves with its SocketConnection when subscribed to its channel and room.
broadcast(name, roomId, event, payload) Validates the event with the serverToClient schema and publishes it to the broker; each room peer receives it at most once.

SocketConnection has receive(message) for each client message, emit(event, payload) and close(code?, reason?). A message that is not JSON or lacks event and payload closes the peer with 1008 and receive rejects with INVALID_SOCKET_MESSAGE.

Examples#

Broadcast from a task or API#

ts
// server/api/maintenance.post.ts
export default defineApiRoute({
  auth: "required",
  body: Notice,
  response: Ok,
  handler: async ({ body }) => {
    await socket.chat.broadcast("general", "message", { text: body.text });
    return { ok: true };
  },
});

In an application, the socket auto-import wraps its hub. In a custom host, call hub.broadcast("chat", "general", "message", { text: "Mantenimiento en 5 minutos" }).

Multiple instances with Redis#

ts
import { createClient } from "redis";
import { createSocketHub, RedisSocketPubSub } from "@meekojs/meeko/server";

const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();
await Promise.all([publisher.connect(), subscriber.connect()]);

const hub = createSocketHub({ channels, sessions, services: {}, pubsub: new RedisSocketPubSub({ publisher, subscriber, prefix: "tienda:prod:socket:" }) });

A broadcast on one instance reaches peers in the same room on all instances.

Common mistakes#

A payload failing the server schema#

Incorrectts
await hub.broadcast("chat", "general", "message", { body: "hello" });

Rejects with ApiError INVALID_SOCKET_PAYLOAD (400) before publishing: outgoing data is validated too, so the client always receives what its contract specifies.

Correctts
await hub.broadcast("chat", "general", "message", { text: "hello" });

A handshake from another origin#

Incorrectts
await hub.preflight("chat", new Request("http://localhost/_meeko/socket/chat", { headers: { origin: "https://otro.example" } }));

ApiError INVALID_ORIGIN (403). The browser sends Origin on WebSockets, and without this check any page could connect with the user's cookies.

Correctts
createSocketHub({ channels, sessions, services: {}, allowedOrigins: ["https://app.example.com"] });

Limits that are not positive integers#

Incorrectts
createSocketHub({ channels, sessions, services: {}, maxMessageBytes: 0 });

RangeError: Invalid socket limits, when creating the hub.

Correctts
createSocketHub({ channels, sessions, services: {}, maxMessageBytes: 16 * 1024 });

Notes#

  • Portable. Does not depend on the runtime: Node, Bun, Deno and Cloudflare adapters use the same hub with their own WebSocket.
  • Delivery. At most once, without queueing or resending: a disconnected peer loses messages. A peer that does not read (full buffer or broker queue) closes to avoid slowing down the rest of the room.
  • Sessions. The session loads at handshake; revocation closes the peer immediately. Channel authorization also runs on each delivery.
  • Security. Validates events in both directions; an invalid message closes the peer with 1008 without running any handler.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/advanced/createsockethub.md