Auth & Admission
How PlayMesh handles authentication and decides which instances a session joins.
Authentication
PlayMesh does not implement authentication. It provides a hook where your application authenticates the incoming connection and returns a user ID. If the hook throws, the connection is rejected.
ℹ
Without an
onAuthenticate hook, sessions are anonymous - the socket ID is used as userId.The hook
JWT example
Rejecting with an error code
Throw a
PlayMeshError to reject the connection with a machine-readable code the client can branch on - no string matching. A plain Error reaches the client with code AUTH_FAILED and its message; a second login under uniqueUser: 'reject' fails with USER_CONNECTED.Sending auth from the client
Admission
After authentication, the admission hook decides which instances the new session joins. The framework then automatically joins the session before the client connection resolves.
✦
References can be
"domainId/instanceId" paths or bare instance IDs (unique across all domains).Roles
The authentication hook can grant roles to a session (
roles: ['admin']). Inside handlers, session.roles and session.hasRole(role) are available for custom checks, and the requireRole middleware protects events by role - events without a rule stay open to everyone:Role-protected domains and instances
Domains and instances can require roles to join. Every join path enforces it - client join requests, admission, and server-side
session.join(). Instance roles apply on top of the domain's.Changing roles live
Roles can change without reconnecting.
session.setRoles() works on a session on this node; mesh.setUserRoles() updates every session of a user on any node. The new roles apply to the next event (requireRole, role-protected instances), the client is told via onRolesChange and an updated client.session.roles, and the session automatically leaves any instance its new roles no longer allow.One session per user
By default a user can hold multiple concurrent sessions (several devices or tabs). The
uniqueUser option enforces one live session per userId:Both policies are cluster-safe: they are backed by atomic per-user Redis claims (TTL-bound, renewed by presence heartbeats, released with owner validation). Two simultaneous logins are deterministic -
reject admits exactly one across all nodes, and replacekeeps the newest accepted connection and kicks exactly the previous one. A crashed node's claim expires on its own, so users are never permanently blocked. The kick reason is Signed in from another connection with code REPLACED:Client join requests
Clients can ask to join an instance with
client.join(path). These requests are denied by default - the server stays authoritative. Opt an instance in with a veto hook:Return
true to admit; return falseor throw to veto (a thrown error's message is sent to the client; the code is JOIN_DENIED, or yours when you throw a PlayMeshError). Server-side session.join(...) and admission are unaffected. client.leave(path) is always honored.Manual assignment
Connection flow
1. Client connects with auth payload
2. onAuthenticate hook - verify token, return userId
3. Session created
4. onSessionCreate hooks run
5. onConnect hooks run
6. onAdmission hook - return instance list
7. Session joins each instance (join hooks fire)
8. playmesh:session event sent to client
9. client.connect() resolves with SessionInfo
Message signing
Opt-in integrity for the wire. When both sides enable
signing, every application event in both directions and every data-bearing protocol message (scoped events, requests and responses, chat, state changes and snapshots, join snapshots, presence) is signed with ECDSA P-256 / SHA-256 (WebCrypto, no extra dependencies). Routing information - instance paths, event names, request ids - rides inside the signed payload, so it cannot be altered without breaking the signature. It must be enabled on both sides - a signing server rejects unsigned clients and vice versa, with no silent downgrade to unsigned mode.Three protocol messages stay deliberately unsigned:
playmesh:session (it bootstraps the key exchange itself - its integrity rests on TLS), playmesh:error (must be deliverable when signing is what failed), and playmesh:kicked (immediately followed by a forced disconnect).Replay protection
Every signed envelope is bound to a random session nonce the server generates per connection (a reconnect gets a new one), the direction and event name, and a strictly increasing sequence number. Together these prevent replay within a session, across reconnects, and against other server nodes. Messages that fail verification are dropped - the wire error is always a generic
Signature verification failed, sessions accumulating failures are disconnected, and on the client failures are reported via onError.Payload restrictions
Signed payloads must be JSON-compatible (objects, arrays, strings, booleans,
null, finite numbers). Date values arrive as ISO strings. Binary values, BigInt, functions, symbols, non-finite numbers and circular structures make emit() throw instead of silently corrupting the message.⚠
Signing does not replace TLS - keys and nonces are exchanged over the transport at connect time, so production must use
https/wss. And it does not prevent cheating: a malicious client signs its own payloads with a valid key, so the server must still validate and authorize everything.ℹ
In multi-node deployments the server's signing keypair is shared between nodes through Redis (stored unencrypted at
<namespace>:signing:keypair, default playmesh:signing:keypair), so treat Redis as part of the trust boundary: require auth, keep it on a private network, and never log or dump that key. There is no automatic key rotation - to rotate, delete the key and restart the cluster.