# Loki > Hosting and host-authoritative multiplayer for finished browser games. Loki hosts a scanned, sandboxed browser build and runs rooms on the same playable URL. Creators do not upload a backend. ## Safety Installing Loki is not a finished game. Each title still requires fine-tuning after installation: inspect real modes, confirm room type and player counts, add a usable create/join flow, tune the reducer or RealtimeProfile, and playtest reconnect and mobile. Agents must not treat npm install or lokiplay ship as a complete integration. ## Packages - @lokiplay/sdk@0.3.8 — JavaScript client - lokiplay@0.3.8 — CLI: login, connect, validate, ship - @lokiplay/ui-web@0.3.8 — optional overlay (no create/join) - @lokiplay/mcp@0.3.8 — agent tools, not for the game bundle ## Start - Docs: https://docs.lokiplay.cc/ - Quickstart: https://docs.lokiplay.cc/quickstart - Agents: https://docs.lokiplay.cc/agents - MCP: https://docs.lokiplay.cc/mcp - App: https://app.lokiplay.cc/ - Play: https://play.lokiplay.cc/ - API: https://api.lokiplay.cc ## Rules - Use @lokiplay/sdk. Do not import Nakama or Socket.IO. - createHostedLokiClient({ projectId }) at boot on hosted play pages. - createRoom() and joinRoom({ inviteCode }). Loki issues 6-digit codes. - createSynchronizedRoom() for turns/events; createRealtimeRoom() for continuous host simulation. Not both in one mode. - Calibrate realtime rooms. Do not copy snapshotHz into game.json tickRate. - Host authority is casual/unranked, not anti-cheat. - Production multiplayer only from a Loki-hosted build. - Login: LOKI_API_URL=https://api.lokiplay.cc npx lokiplay@0.3.8 login - No inline scripts, remote fonts, or forms. - Layer 2 public catalog is closed. - Native Swift/Kotlin/Unity 0.3.8 clients support synchronized rooms only. ## Full docs https://docs.lokiplay.cc/llms-full.txt # Full documentation The HTML docs live at https://docs.lokiplay.cc/ with the same sitemap as this file. Every HTML page repeats the fine-tuning safety note: install is not a finished game. # AGENTS.md # Loki integration rules - Install and use `@lokiplay/sdk`; do not import Nakama APIs into game code. - Production multiplayer runs only from a Loki-hosted finished browser build. - Upload `game.json`, `index.html`, and static assets. Do not upload source-only repositories, backend processes, secrets, creator ad scripts, or localhost dependencies. - MVP multiplayer is host-authoritative. Loki controls identity, tenant boundaries, membership, matchmaking, sequencing, snapshots, and host migration. - Never trust or override the `projectId`, player identity, room membership, or sequence returned by Loki. - Create rooms with `createRoom()` and join with `joinRoom({ inviteCode })`. Do not invent Loki room keys. - Installing the SDK does not add a create/join screen. If the game has no usable room-entry flow, add one before shipping: a minimal lobby (create room, join with invite, copy invite, start when ready) or an automatic flow (plain URL creates a room; invite or deep-link URL joins it). The Loki overlay shows room status, players, invite copy, and chat only; it does not create or join rooms. Players still need loading, waiting, and error states. - Before configuring Loki multiplayer, inspect the game's source, existing UI, configuration, documentation, tests, and finished build. Locate its game modes, seats, local-player handling, AI opponents, teams, start conditions, turn or update loop, win conditions, reconnect behavior, and existing networking code. - Do not infer multiplayer requirements from the game's name, genre, appearance, or common rules. A chess, pool, racing, or strategy game may support different player and team arrangements. - Determine requirements separately for every supported game mode. Do not collapse multiple modes into one profile. - Record evidence for each conclusion and distinguish observed facts from creator decisions. If any material field is ambiguous, stop and ask the creator. Never silently choose a player count, team arrangement, simulation model, authority model, update frequency, persistence policy, or matchmaking flow. - Determine and confirm for each mode: minimum, recommended, and maximum players; number of teams, team size, and whether players share control; private invite, lobby, matchmaking, or asynchronous entry; whether late joining and spectators are allowed; turn-based, event-driven, continuous realtime, or hybrid simulation; sequential or simultaneous input; required authoritative update frequency and latency sensitivity; session duration and persistence requirements; host-authoritative trust tolerance or server-authority requirement. - Classify simulation from how authoritative state progresses, not from visual animation. A game animated at 60 FPS may still be turn-based or event-driven. - Preserve existing game modes and rules. Add online settings and entry UI from the confirmed profile, including mode selection, team or seat selection, readiness, player limits, invite and join behavior, waiting states, and start conditions. - Do not invent new `game.json` fields. Current manifests accept only `enabled`, `authority`, `maxPlayers`, and `tickRate`. Report the richer profile in the final report: values, supporting evidence, and creator-confirmed decisions. - After inspecting and confirming each game mode's profile, choose `createSynchronizedRoom()` for turn-based or event-driven state, or `createRealtimeRoom()` for continuous host-authoritative simulation. Do not choose by genre or animation smoothness; choose by how authoritative state actually progresses. Do not run both room types for the same mode. - `createSynchronizedRoom()`: define project-owned state and actions, then provide a reducer. Do not implement a parallel authority, version, or membership protocol. - Keep shared state JSON-compatible and use finite safe integers. Reducers must be synchronous, deterministic, and fast. Do not perform rendering, timers, network calls, or other I/O inside a reducer. Keep transient visual and interpolation data out of synchronized state. - Subscribe to synchronized snapshots for state, members, authority, and connection status. Keep the same `LokiClient` and synchronized-room instance while interrupted. Let the SDK own browser lifecycle detection, socket replacement, reconnect retries, snapshot recovery, and pending-action replay. - `createRealtimeRoom()`: integrate the game's existing simulation through `RealtimeRoom`'s callbacks (predict/interpolate/extrapolate/blendCorrection/ shouldCorrect/composeRenderState) rather than writing a parallel input queue, RTT estimator, snapshot pacer, stale-round rejection, input ledger, interpolation buffer, or reconnect netcode; the SDK already owns all of that, including per-submission in-flight accounting (a rejected or silently-dropped snapshot always releases its slot) and genuine `inputHz` pacing for `setInput()` (call it every frame; the SDK paces/coalesces the network send). Keep one game-owned render loop driven by `advanceFrame()`/`getRenderState()` (call each at most once per rendered frame). Keep authoritative snapshots compact and self-contained (no references to transient local-only state). - `snapshotHz` (default/cap 30) is a ceiling, not a delivery guarantee. Do not default to the highest rate. Start conservative — pass `adaptiveRate: true` with `initialSnapshotHz` around 12-15 and `minSnapshotHz` around 8, or an explicit low fixed `snapshotHz` — and raise it only with diagnostic evidence. Prefer `calibrateRealtimeRoom()` over guessing: it exercises candidate rates against a real two-client `RealtimeRoom` pair at a fixed `snapshotHz` (`adaptiveRate: false` on the candidate rooms) and recommends the highest rate that is actually delivered without a Loki-owned cliff (acceptance, delivery match, extrapolation, held authoritative frames, ack latency, backpressure). Optional `evaluate()` can only veto a rate. Write only the returned `RealtimeProfile` into `createRealtimeRoom()` (`adaptiveRate: true`, `minSnapshotHz: 8`, `initialSnapshotHz` 12 or the ceiling if lower). If no candidate qualifies, use that floor profile (8 Hz, adaptive); if the sweep is incomplete, calibration throws — fix the run and repeat, do not invent a profile. `game.json` `tickRate` is a separate Nakama room-loop setting; do not copy `snapshotHz` into it. `publishSnapshot()` already coalesces multiple calls made within the same synchronous turn to a single send; do not build a separate coalescing layer for that. Report the calibrated or selected snapshot/input rates and the observed diagnostics (RTT, jitter, acceptance/rejection ratios, reconnect/ migration duration, dropped/coalesced frames, held authoritative frames, and any `onDiagnosticWarning` events) as evidence, not assumptions. - For multi-entity games, prefer the `createEntityCompositor`/ `createLocalPrediction` helpers over hand-writing `composeRenderState`/ `predict` selection logic; they cover the "predict/compose the local entity, interpolate the rest" plumbing while the game still supplies the selectors for its own State shape. Use `setEntities` (bulk, once per frame) over `setEntity` (once per entity) for games with many entities. Use `sendEffect()`/`onConfirmedEffect()` to confirm authoritative events (e.g. collisions) with a stable id instead of a custom event-confirmation channel, so speculative local particles or audio can be deduped against the confirmed outcome. - For hosted sandbox games, call `createHostedLokiClient({ projectId })` once at page boot and reuse the client it returns for every Create/Join. It installs the `loki:init` listener and requests the session immediately, rather than deferring authentication to a Create/Join button click — deferring it risks the hosted shell's own ~15s "Connecting…" timeout elapsing first. Do not hand-write that handshake. - Do not claim Loki supplies game physics, collision resolution, rendering optimization, or competitive/anti-cheat integrity for `createRealtimeRoom()` games. Loki owns transport, sequencing, fencing, and delivery only; the game owns simulation and rendering. - `createRealtimeRoom()` requires every present room member to be realtime-capable before it activates; a legacy or non-realtime-capable client blocks activation and cannot join an already-active realtime room. Native clients cannot join realtime-mode rooms until a later parity release; do not offer `createRealtimeRoom()` for cross-client modes yet. - Do not implement competing reconnect behavior for `visibilitychange`, `pagehide`, `pageshow`, `blur`, `focus`, `online`, or `offline`. Never call `leave()`, `close()`, transport disconnect, or create a replacement room because the page became hidden, blurred, offline, or unloaded. Call `leave()` only for an explicit user Leave/End Game action. - Call `dispatch()` only while `connection` is `connected`. For `suspended`, `reconnecting`, or `resynchronizing`, lock authoritative input, preserve the last rendered state, show a temporary reconnecting message, and wait for an authoritative snapshot. Do not assume the player is still host afterward. - Loki replays unresolved actions with the same `(senderId, actionId)`. Do not repeat one under a new action ID. Treat `indeterminate` or "authoritative confirmation timed out" as an unknown outcome, not proof of failure. Treat `room_closed` as terminal. Resolve `leave_failed` before starting another room with that client. - Handle rejected actions from `dispatch()` without inventing a parallel protocol. Build and deploy from this repository. - Include ``. Do not globally disable browser zoom. Make the game root `width: 100%`, `height: 100vh`, then `height: 100dvh`, with `overflow: hidden`. Account for `env(safe-area-inset-*)`; gameplay must not require document scrolling, though internal menus may scroll. - Recalculate layout from the game container on resize. Handle `visualViewport.resize` when available and `orientationchange` as a fallback. Preserve logical game coordinates and fit them to the available area instead of hard-coding desktop pixels. Support both orientations unless the game clearly explains a required orientation. - For canvas games, separate CSS size from backing resolution, scale the backing store by `devicePixelRatio` with a reasonable cap such as 2, and resize and redraw after viewport changes without replacing the canvas node. Keep critical controls inside safe-area boundaries. - Use Pointer Events for touch, mouse, pen, and trackpad. Apply `touch-action: none` only to a direct-manipulation playfield, use pointer capture for dragging or aiming, handle `pointercancel` and lost capture, make primary targets at least 44x44 CSS pixels, and do not rely on hover. - Use one controlled `requestAnimationFrame` rendering loop. Pause or throttle rendering while hidden without leaving the Loki room, then render the latest authoritative snapshot. Cap canvas resolution and avoid allocating large buffers every frame. - Loki-hosted games run in a sandbox iframe with a strict CSP. Do not use inline `