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 / Property | Description | Returns |
|---|---|---|
port | Port to listen on. Defaults to 3000. Pass 0 for ephemeral port. | number |
server | Attach to a pre-existing HTTP server. | http.Server |
redis | Redis connection options or URL string. Enables distributed mode. | RedisOptions | string |
namespace | Redis key namespace isolating this deployment. Defaults to 'playmesh' (the pre-0.6 keys). Deployments with different namespaces never interact. | string |
socket | Options forwarded to the Socket.IO server. | Partial<ServerOptions> |
signing | Sign all application events with ECDSA P-256. Clients must enable signing too. | boolean |
uniqueUser | One live session per userId: 'replace' kicks existing sessions, 'reject' refuses the new login. Omit to allow concurrent sessions. | 'replace' | 'reject' |
serialization | Wire encoding. 'msgpack' uses socket.io-msgpack-parser (binary, smaller frames); every client must set the same value. Defaults to 'json'. | 'json' | 'msgpack' |
Topology
| Method / Property | Description | Returns |
|---|---|---|
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 |
domains | All registered Domains. | Domain[] |
sessions | All 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 / Property | Description | Returns |
|---|---|---|
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 / Property | Description | Returns |
|---|---|---|
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 / Property | Description | Returns |
|---|---|---|
io | The underlying Socket.IO server. Available after start(). | SocketIOServer |
redis | The shared Redis client (when Redis is configured). | Redis |
queues | The 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 / Property | Description | Returns |
|---|---|---|
id | The domain identifier. | string |
instances | All 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 / Property | Description | Returns |
|---|---|---|
id | The instance identifier. | string |
domain | The owning Domain. | Domain |
path | "domainId/instanceId" - globally unique reference. | string |
sessions | Sessions on this node that are members. | Session[] |
state | Server-only runtime key-value state. | ScopedState |
publicState | State synced live to every client in this instance. | ScopedState |
userState(userId) | Private state synced only to that user's clients. | ScopedState |
temporary | Whether 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 / Property | Description | Returns |
|---|---|---|
id | Socket ID (unique per connection). | string |
userId | Assigned by your authenticate hook. | string |
roles | Current 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 |
data | Application data from the auth result. | Record<string, unknown> |
address | The client's address as seen by the server. | string |
connectedAt | When this connection was established (epoch ms). | number |
instances | Instances 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 / Property | Description | Returns |
|---|---|---|
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.