# Multiplayer and shared apps Hackemist provides a shared-session SDK for browser games, quizzes, collaborative tools, and chat. Creators integrate it in their static browser build; publishing a single-player game does not automatically add multiplayer rules. External authoritative game servers can still use operator-approved API/WebSocket origins. The separate `realtime` service handles room messages in memory without a SQLite write for every event. The control service authorizes a short-lived, launch-bound connection ticket; the injected SDK obtains it through the existing source/origin/nonce-checked player bridge. Account cookies, account bearer tokens, emails, and global user identifiers are never provided to app code or other participants. Tickets are sent in the first WebSocket message, never URLs. ## Enable an app 1. Open the app's Studio → App details → Browser compatibility. 2. Under **Multiplayer & connections**, select **Enable shared multiplayer rooms, presence, and messaging**, then **Save multiplayer option**. Creators can activate the first-party room service when the operator enables creator options and the configured room service. An administrator can also review the complete permission profile or save it after password confirmation. 3. Reopen the app. The runtime grants only the configured room-service WebSocket endpoint. Other external servers still need exact-origin permission. Approved multiplayer also permits local JavaScript form events inside the sandbox. Network form submission stays blocked by `form-action 'none'` unless an external form origin is separately approved. Popup and top-level navigation restrictions remain in place. 4. Administration → Multiplayer controls whether the service is enabled and whether guests may participate in public apps. Signed-in accounts are required by default. Private and premium projects retain their normal access requirements; an invitation never grants app entitlement. Rooms are separated by project, version, and preview/live mode. A preview cannot join a public room, and a project cannot discover another project's rooms by guessing a code. A newly published incompatible version cannot join an older version's room. Participants can only access apps allowed by their current account and launch; logout, deletion, expired access, permission changes, and restore-invalidated launches close sockets at the configured access-check interval. ## Creator SDK The injected `window.Hackemist` SDK includes `multiplayer` since version 1.2.0; the current SDK is 1.3.0. Use the existing player; standalone file previews do not grant shared-session access. ```js const client = await Hackemist.multiplayer.connect(); const invitation = await Hackemist.multiplayer.invitation(); const room = invitation ? await client.joinRoom(invitation) : await client.createRoom({maxPeers: 8}); console.log(room.code, room.invite_url); client.on('state', snapshot => renderSharedState(snapshot.state)); client.on('broadcast', event => handlePlayerEvent(event)); // Events identify the sender using the broker's peer ID. await client.send('chat', {text: 'Hello!'}); // Only the current host can update shared state. Supply the known revision. await client.setState({round: 1}, client.revision); // Finish participation explicitly; the transport also closes on page exit. await client.leaveRoom(); client.close(); ``` The source repository includes a working quiz/chat at `examples/multiplayer-quiz/index.html`. It includes create/join, invitation copying, a player list, questions/answers/scores, safe chat rendering, room locking, kicking, host transfer, and reconnect status. Import that HTML through the normal creator workflow and enable multiplayer. MCP creators use `runtime_options_get` and `runtime_options_update` with the current `policy_revision`; separately hosted services use reviewed `connections_request`. See [creator connections](/docs/connections.md) for the shared Studio/MCP workflow. The broker enforces membership, sender identity, host-only operations, and compare-and-set shared-state revisions. It does not run uploaded game rules. The sample uses a trusted host to score answers. Competitive games requiring independent anti-cheat, secret answers, persistent economies, payments, or authoritative physics should use a separately hosted authoritative backend. Do not put server secrets in a static build. ## Invitations, presence, and recovery - Share the app invitation (`/app/PROJECT_ID?room=CODE`), not a runtime launch or connection ticket. The SDK reads a validated invitation from its parent player and joins after normal app authorization. - Codes use cryptographically random characters. Rooms can be locked, and the host can remove a participant or transfer hosting. Leaving explicitly removes membership; an unexpected disconnection reserves it for the configurable grace period. - The SDK fetches fresh authority for reconnects, reports disconnected/stale room state, and rejoins during the grace period. Writes are never silently replayed. Resolve stale revisions by reading the latest room state before issuing a new update. - Shared state and bounded event history are temporary. Room expiry, moderation closure, or a service restart removes them. Persist lasting player progress through the save SDK; an app can also use its approved external database. - Treat player messages as untrusted text and data. Use text nodes, validate your app's action schema, and never render chat with `innerHTML`. ## Administrator controls Administration → Multiplayer shows connection/room/buffer counts, a bounded sample of room metadata, and an **End room** action with an audited reason. It does not display chat or room-state contents. Capacity settings cover concurrent sockets, rooms, per-account/per-launch/per-app quotas, and room size. Message settings cover payload/state size, account and connection rates, room/global outgoing-byte rates, per-player queues, total buffered bytes, and recent history. Session settings cover room lifetime, reconnect grace, idle timeout, ticket lifetime, and access rechecks. Lowering a limit does not create extra server capacity; tune it using the load test on the intended host. Default ceilings are 2,000 connections, 1,000 rooms, and 32 players per room. These are admission limits, not a measured capacity promise. Individual queues and aggregate buffers are bounded; a slow recipient is dropped independently. Room and global fanout budgets reject excessive traffic before shared-state mutations commit. ## Docker and Dokploy Local Compose exposes the service on `127.0.0.1:8002`; the browser uses `ws://localhost:8002/realtime/ws`. Production Compose keeps port 8002 private. Caddy routes only `/realtime/ws` on the runtime HTTPS origin to this service, strips account credentials, and supports WebSocket upgrades. Internal metrics and administrative closure routes are not forwarded publicly. Keep exactly **one realtime worker/replica** with the supplied in-memory broker. Multiple independent replicas would partition rooms. A future horizontal deployment requires a shared room backend and coordinated authority/fanout; increasing Uvicorn workers is not that migration. The service has separate memory/CPU/process limits and a bounded pre-ASGI WebSocket parser. Rooms end during a service restart or deploy; announce maintenance for long-running competitions. The control/runtime deployment remains one Linux host with SQLite WAL and private immutable objects. Blocking player/database operations use bounded worker admission, with health/read paths independent of queued writers. Generic API quotas distinguish authenticated accounts from shared anonymous IP traffic. Local unchanged immutable assets reuse an inode/size/time/digest verification proof; changed files are verified again. S3 files remain fully verified and use process-wide byte/parallel admission held until streams close. `S3_MAX_VERIFIED_DOWNLOADS` and `S3_MAX_VERIFIED_BYTES` must accommodate the largest served file and available temporary disk. `RUNTIME_MAX_REQUESTS` caps complete in-flight asset responses. Docker builds SQLite 3.53.4 from a digest-verified upstream amalgamation, including the [WAL-reset concurrency fix](https://www.sqlite.org/wal.html#walresetbug). Existing durability and optimistic-save checks are retained. Small progress saves and heartbeats use bounded batches to amortize durable disk commits. Every operation has its own fresh authorization and savepoint; a conflict or invalid operation does not roll back another player's valid save. Success is sent only after the batch's FULL commit completes. Queue count, estimated memory, batch size, and waiting time are capped; unknown commits are never automatically replayed. Healthy signed-in heartbeats renew launch expiry after fresh session and permission checks. Expired/revoked launches and previews do not gain a new deadline. ## Verification ```sh python -m unittest tests.test_realtime tests.test_concurrency tests.test_write_batch tests.test_transfer_concurrency -v npm run test:multiplayer-sdk ``` `npm run test:multiplayer-browser` is an opt-in real-deployment test using three isolated browser contexts. Set `HACKEMIST_BASE_URL`, private administrator test credentials, and `HACKEMIST_ALLOW_MUTATIONS=1` against a local or disposable staging deployment. It creates, approves, publishes, exercises, and removes its own quiz app. Set `HACKEMIST_KEEP_MULTIPLAYER_DEMO=1` only when deliberately retaining that sample. Do not log credentials or tickets. The concurrency report (operator repository reference: `CONCURRENCY.md`) records actual measurements and remaining deployment limits. Repeat its disposable test on comparable production resources: networking, CPU, storage latency, graphics performance on each player's device, and app-specific rules all affect experience.