PlayMesh/Docs
HomeGitHubnpm

Server API

Complete reference for @playmesh/server - PlayMesh, Domain, Instance, and Session.

PlayMesh

The root class. Import from @playmesh/server.

Constructor

import { PlayMesh } from '@playmesh/server';
const mesh = new PlayMesh(options?: PlayMeshOptions);
Method / PropertyDescriptionReturns
portPort to listen on. Defaults to 3000. Pass 0 for ephemeral port.number
serverAttach to a pre-existing HTTP server.http.Server
redisRedis connection options or URL string. Enables distributed mode.RedisOptions | string
namespaceRedis key namespace isolating this deployment. Defaults to 'playmesh' (the pre-0.6 keys). Deployments with different namespaces never interact.string
socketOptions forwarded to the Socket.IO server.Partial<ServerOptions>
signingSign all application events with ECDSA P-256. Clients must enable signing too.boolean
uniqueUserOne live session per userId: 'replace' kicks existing sessions, 'reject' refuses the new login. Omit to allow concurrent sessions.'replace' | 'reject'
serializationWire encoding. 'msgpack' uses socket.io-msgpack-parser (binary, smaller frames); every client must set the same value. Defaults to 'json'.'json' | 'msgpack'

Topology

Method / PropertyDescriptionReturns
createDomain(id, options?)Create and register a new Domain. { roles: [...] } restricts joining to those roles.Domain
domain(id)Get an existing Domain by id. Throws if not found.Domain
hasDomain(id)Check whether a Domain with this id exists.boolean
domainsAll registered Domains.Domain[]
sessionsAll active Sessions on this node.Session[]
resolveInstance(ref)Resolve "domainId/instanceId" or a bare instance id.Instance
getDomain(id)Non-throwing lookup: Domain or undefined.Domain | undefined
getInstance(ref)Non-throwing lookup by path or unique bare id.Instance | undefined
getSession(sessionId)Session by id on this node.Session | undefined
getUserSessions(userId)All of a user's sessions on this node.Session[]

Hooks

Method / PropertyDescriptionReturns
bootstrap(hook)Async setup before the server accepts connections.this
onAuthenticate(hook)Register the authentication hook.this
onAdmission(hook)Decide which instances a session joins.this
onChatMessage(hook)Moderate built-in chat. Runs once per destination instance (ChatContext.instance); return true, a rewritten string, or false/throw to block that delivery.this
onSessionCreate(hook)Called once per session, after authentication.this
onConnect(hook)Called when a session connects.this
onDisconnect(hook)Called when a session disconnects.this
onStarted(hook)Called after the server is listening.this
onShutdown(hook)Called at the start of graceful shutdown.this
on(event, handler)Mesh-wide event handler: receives the event from any session, even one in no instance, after instance and domain handlers. For shop, wardrobe, lobby and admin events.this
off(event, handler)Remove a mesh-wide handler.this

Messaging & extensions

Method / PropertyDescriptionReturns
broadcast(event, payload?)Send an event to every connected session across all nodes.void
sessionsOf(userId)Session ids of a user across all nodes.Promise<string[]>
kickUser(userId, reason?, code?)Kick every session of a user, on any node.void
moveUser(userId, to, from?)Move every session of a user, on any node, to the instance at path to (see session.move).Promise<void>
setUserRoles(userId, roles)Replace the roles of every session of a user, on any node (see session.setRoles).Promise<void>
use(middleware)Register a middleware function.this
use(plugin)Install a plugin with an install() method.this

Infrastructure & lifecycle

Method / PropertyDescriptionReturns
ioThe underlying Socket.IO server. Available after start().SocketIOServer
redisThe shared Redis client (when Redis is configured).Redis
queuesThe QueueManager (requires Redis).QueueManager
start()Start the server.Promise<{ port: number }>
shutdown()Gracefully shut down.Promise<void>
metrics()Current runtime metrics.Metrics
inspect()Admin view: every domain and instance with its cluster-wide members, plus this node's sessions (roles, address, memberships).Promise<MeshSnapshot>
const mesh = new PlayMesh({ port: 4000, redis: { host: 'localhost', port: 6379 } });

mesh.bootstrap(async ({ mesh }) => { /* create domains and instances */ });
mesh.onAuthenticate(async request => ({ userId: request.auth.userId as string }));
mesh.onStarted(() => console.log('Server ready'));

const { port } = await mesh.start();

process.on('SIGTERM', () => mesh.shutdown());

Domain

Retrieved via mesh.createDomain(id) or mesh.domain(id).
Method / PropertyDescriptionReturns
idThe domain identifier.string
instancesAll Instances in this domain.Instance[]
createInstance(id, options?)Create a new Instance. { autoDestroy: ms | true } makes it temporary; { roles: [...] } restricts joining (on top of domain roles).Instance
on(event, handler)Domain-level event handler - runs for events from sessions inside this domain.this
off(event, handler)Remove a domain-level handler.this
findOrCreate({ maxMembers, filter?, id?, instance?, join? })Matchmaking: the first instance passing filter with fewer than maxMembers members (cluster-wide), or a new one ({ autoDestroy: true } by default). join seats a session in the same step; calls are serialized per domain on this node.Promise<Instance>
instance(id)Get an Instance by id.Instance
hasInstance(id)Check whether an Instance exists.boolean
destroyInstance(id)Remove sessions, clear state, delete the instance.Promise<void>
broadcast(event, payload?)Send to every session in any instance of this domain.void
onInstanceCreated(hook)Called when a new instance is created.this
onInstanceDestroyed(hook)Called when an instance is destroyed.this

Instance

An isolated multiplayer environment. Sessions join to receive broadcasts and have events routed.
Method / PropertyDescriptionReturns
idThe instance identifier.string
domainThe owning Domain.Domain
path"domainId/instanceId" - globally unique reference.string
sessionsSessions on this node that are members.Session[]
stateServer-only runtime key-value state.ScopedState
publicStateState synced live to every client in this instance.ScopedState
userState(userId)Private state synced only to that user's clients.ScopedState
temporaryWhether this instance auto-destroys when it empties out.boolean
on(event, handler)Register an event handler.this
off(event, handler)Remove an event handler.this
onRequest(event, handler)Answer client.requestTo(...) requests; the return value is the response (one handler per event). throw new Error(msg) rejects the client promise with msg.this
broadcast(event, payload?)Send to all member sessions across all nodes.void
broadcastWhere(filter, event, payload?)Send only to members matching filter({ sessionId, userId }), across all nodes - e.g. players in range.Promise<void>
memberCount()Presence-backed total member count across all nodes.Promise<number>
members()Members across all nodes as a sessionId → userId map.Promise<Record<string, string>>
onJoin(hook)Called when a session joins.this
onLeave(hook)Called when a session leaves.this
onMemberLeft(hook)Called for every departure - hook({ sessionId, userId }, reason) with reason 'leave' | 'disconnect' | 'ghost'. Ghosts are sessions of a crashed node, reaped once cluster-wide. The one place to clean up per-player state.this
onJoinRequest(hook)Allow/veto client-initiated joins. Without this hook, client joins are denied.this
ℹ
instance.sessions is local-node only. Use instance.memberCount() or instance.members() for the cluster view.

Session

Represents a single connected client.
Method / PropertyDescriptionReturns
idSocket ID (unique per connection).string
userIdAssigned by your authenticate hook.string
rolesCurrent roles, from the authenticate hook or setRoles().readonly string[]
setRoles(roles)Replace roles live, without reconnecting. Notifies the client and leaves instances the new roles no longer allow.Promise<void>
hasRole(role)Check whether the session holds a role.boolean
dataApplication data from the auth result.Record<string, unknown>
addressThe client's address as seen by the server.string
connectedAtWhen this connection was established (epoch ms).number
instancesInstances this session is currently a member of.Instance[]
isIn(instance)Check membership by Instance or id string.boolean
join(instance)Join an instance. No-op if already a member.Promise<void>
leave(instance)Leave an instance. No-op if not a member.Promise<void>
move(to, from?)Leave from (default: every instance in the target's domain), then join to. The client hears onLeave then onJoin. Throws ROLE_REQUIRED before leaving anything if roles don't allow to.Promise<void>
send(event, payload?)Send an event to this client only.void
kick(reason?, code?)Notify the client why (playmesh:kicked, with a code such as KICKED), then force-disconnect it.void
disconnect()Forcibly disconnect this client.void

Typed events

Both SDKs accept event-map generics that type emit, on, send, and broadcast payloads end to end, plus a third request-map generic typing onRequest / requestTo. Compile-time only - still validate payloads at runtime.
type ClientEvents = { 'player:move': { x: number; y: number } };
type ServerEvents = { 'player:update': { x: number; y: number; by: string } };
type Requests = {
  'shop:buy': { request: { itemId: string }; response: { success: boolean; balance: number } };
};

const mesh = new PlayMesh<ClientEvents, ServerEvents, Requests>();
city.on('player:move', (session, payload) => {
  // payload is { x: number; y: number }
  session.send('player:update', { ...payload, by: session.userId });
});

shop.onRequest('shop:buy', async (session, payload) => {
  // payload is { itemId: string }; return type is checked
  return { success: true, balance: 950 };
});

ScopedState

Method / PropertyDescriptionReturns
get(key)Get a value. Returns undefined if not set.Promise<unknown>
set(key, value, { visibleTo? })Set a value (JSON-serialized). On publicState, visibleTo: userIds sends the value only to those users; everyone else sees the key as deleted.Promise<void>
patch(entries, { visibleTo? })Set many keys and delete those whose value is undefined, as one atomic change: one revision, one sync message.Promise<void>
delete(key)Delete a key.Promise<void>
keys()List all keys in this instance scope.Promise<string[]>
entries()All key-value pairs in this instance scope.Promise<Record<string, unknown>>
clear()Delete all keys in this instance scope.Promise<void>
increment(key, delta, { min?, max? })Atomically add delta (missing keys count as 0), clamped to bounds. Returns the new value. Lua-atomic on Redis.Promise<number>
compareAndSet(key, expected, value)Atomically set if the current value equals expected (canonical JSON). expected: undefined matches only a missing key.Promise<boolean>
setIfAbsent(key, value)Atomically set only if the key is missing.Promise<boolean>
Synced scopes (publicState, userState) are revisioned: every mutation carries a monotonic per-scope revision that clients use to apply changes in order and recover from gaps. See Instance State for details.

Errors

Every error a client sees carries a machine-readable code. Throw PlayMeshError from any hook or handler to send your own; a plain Error is reported as APP_ERROR.
import { PlayMeshError, type ErrorCode } from '@playmesh/server';

shop.onRequest('shop:buy', () => {
  throw new PlayMeshError('NOT_ENOUGH_COINS', 'You need 10 coins');
});
Built-in codes: VERSION_MISMATCH, AUTH_FAILED, USER_CONNECTED, SIGNING_REQUIRED, SIGNATURE_INVALID, CONNECTION_FAILED, ROLE_REQUIRED, RATE_LIMITED, JOIN_DENIED, TARGET_DENIED, REQUEST_DENIED, INVALID_PAYLOAD, CHAT_REJECTED, APP_ERROR, INTERNAL. Kicks use KICKED, REPLACED, RATE_LIMITED or yours.

Metrics & inspection

const m = mesh.metrics();
// {
//   sessions: number,        - active sessions on this node
//   domains: number,
//   instances: number,
//   uptimeMs: number,
//   eventsReceived: number,  - inbound client messages since start
//   errors: Record<string, number>, - errors sent to clients, by code
// }

const snapshot = await mesh.inspect();
// { nodeId, metrics, online, domains: [{ id, instances: [{ path, temporary, members }] }],
//   sessions: [{ id, userId, roles, address, connectedAt, instances }] }
ℹ
Roles and addresses are only known on the node holding the session; members and online are cluster-wide.