# Rogue Realtime

Build multiplayer games and collaborative apps with project rooms, live presence,
ephemeral events and durable JSON state. Each room is isolated and ordered by its
own coordinator. A room can represent a game session, whiteboard or shared task.

Discover deployment availability and current limits at
`GET /api/v1/realtime/capabilities`, and the complete frame contract at
`GET /api/v1/realtime/protocol`. Bootstrap includes both links. Rooms are project
resources. Public projects do not automatically expose their room data.

## Create and inspect a room

Use `rog` 0.17.0+ or the equivalent MCP tools. Check the live client download
catalog for published package availability; server and client versions differ.

```sh
rog realtime capabilities
rog realtime list PROJECT_ID
rog --data '{"name":"playtest","max_connections":8}' realtime create PROJECT_ID
rog realtime get ROOM_ID
rog realtime state ROOM_ID
rog realtime watch ROOM_ID
```

`agent:read` and current project membership allow metadata, state and a ticket for
your own participant connection. `projects:write` permits room creation, policy
edits, deletion, trusted state writes, events, kicks and app-user ticket issuance.
Room capacity is charged to the project owner. A managed Skills project cannot
contain game rooms. `get_project_resources` includes rooms only for members.

A policy edit requires `expected_revision` and disconnects existing sockets:

```sh
rog --data '{"expected_revision":1,"state_policy":"server"}' realtime update ROOM_ID
rog --data '{"expected_revision":2}' realtime delete ROOM_ID --yes
```

Deletion removes the catalog entry, closes connections and queues durable storage
cleanup. The queue survives project/account deletion and provider outages.
After an uncertain create or policy response, list/read the room before retrying.

## Browser and app identity

Keep your Rogue API key on your trusted backend. Validate your own app's user
session before calling `create_realtime_app_ticket` (REST
`POST /api/v1/realtime/rooms/{id}/app-tickets`) with:

```json
{"user_id":"app-user-123","origin":"https://game.example","role":"participant","info":{"name":"Sam"}}
```

The origin must appear in the room's `allowed_origins`. Return only the resulting
room ticket to the browser. `viewer` tickets can receive state, presence and
events but cannot write. `participant` tickets can update presence and events;
state writes also depend on the room's policy. `app:` identities and their info
are assertions by the project backend, not Rogue account identities.

A member obtains their own ticket at `POST .../ticket` with `{}` for a native
client, or `{ "origin": "https://game.example" }` for a browser.
Tickets expire after **60 seconds** and are usable once. Request a new ticket
after **40 seconds** and send `{ "t":"auth", "token":"..." }` to renew in place.
Auth renewal must preserve the connection's subject and origin.

```js
const socket = new WebSocket(ticket.websocket_url, [
  "rogue.realtime.v1", `rogue.ticket.${ticket.token}`
]);
```

The returned `websocket_url` contains the room UUID, never a credential. Native
clients may instead send `Authorization: Bearer <ticket>` with the protocol
subheader. Never use a long-lived API key in a WebSocket URL or browser bundle.

Room policy changes invalidate old tickets immediately and close existing
sessions when the runtime receives the new revision. Membership, key, account
and project visibility changes prevent fresh admission; existing leases expire
within 60 seconds. A failed policy propagation also falls back to this lease.
Kicking a connection does not ban its identity or revoke its right to rejoin.

## Public guest games

Guest access is explicit, requires a public project, and uses one of two paths:

- A separately hosted game lists exact HTTPS `allowed_origins` (HTTP localhost
  is accepted for development). Use `guestTicketSource(roomId)` from the SDK.
- A Rogue-published public app lists its UUID in `allowed_publication_ids` and
  uses `rogue.realtimeTicket(roomId, options)` through its trusted parent frame.
  The JavaScript SDK chooses this bridge automatically. Add `rogue.camp` to the
  publication's `connect_hosts` for the WebSocket. The page and room must belong
  to the same public project. Custom domains must be active Rogue claims.

```sh
rog --data '{"expected_revision":1,"guest_access":true,"allowed_origins":["http://127.0.0.1:5173"],"allowed_publication_ids":["PUBLICATION_UUID"]}' realtime update ROOM_ID
```

The direct endpoint is `POST .../guest-tickets`; the parent bridge uses
`POST .../publication-tickets` with `publication_id`. Both check current policy.
The latter produces an opaque-origin ticket only for the explicitly admitted
public app. It grants no Rogue account access. Existing publication API rules
still reject authenticated requests and mutations from the isolated frame.

Guests get server-assigned identities. Keep the returned `session_token` in
memory and send it in later ticket requests to preserve identity for up to 24
hours. It is bound to the room, origin and (when applicable) publication. Starting
without it creates a new guest identity. Guest issuance is limited to 360
requests/minute per room and hashed IP, including renewal. Origin checks are app
admission controls, not proof of a person; public guest rooms are unsuitable for
private or trusted state.

## JavaScript and React

The official JavaScript client provides a browser-safe room client. For a custom
app backend, supply a callback that returns its freshly authorized ticket:

```js
import { RealtimeRoom, guestTicketSource } from "@rogue-camp/client";
const room = new RealtimeRoom({
  getTicket: guestTicketSource(roomId), // explicitly public guest game
  // For private app users, use your authenticated backend callback instead.
});
await room.ready;
const stop = room.subscribe(() => render(room.getSnapshot()));
room.presence({ cursor: { x: 30, y: 40 } });
const stopEvents = room.on("event", event => handleGameEvent(event));
room.broadcast("player.jump", { height: 2 });
await room.patch([{ op: "increment", key: "visits", by: 1 }]);
// On unmount: stop(); stopEvents(); room.close();
```

A Node/member client may use `client.realtime(roomId)` to obtain and renew its own
tickets. The SDK reauthorizes after network reconnects and receives a fresh
snapshot. It never replays gameplay events or uncertain state patches. Policy
closures, deletion and capacity rejection require an explicit reconnect after
the application handles the cause. An initial authorization failure is surfaced.

With React, create the room in an effect, close it on cleanup, and subscribe with
`useSyncExternalStore(room.subscribe, room.getSnapshot, room.getSnapshot)` in the
mounted child. Keep connection creation outside rendering; strict-mode effect
cleanup must close the abandoned connection. State snapshots change identity
only when a new update arrives. `on("error", handler)` exposes failures.

Python, Go, Rust, Ruby, PHP, C and C++ provide ticket-based sockets and all generated
control-plane tool helpers. Their low-level transports do not create background
renewal threads: renew with an `auth` frame every 40 seconds while receiving.
Use a fresh ticket and snapshot when reconnecting, and serialize socket writes.

## Presence, events and state

The server first sends `hello` with your connection ID, subject, role, expiry,
full state `{version,values}` and all live connections. A user can have several
connections, each with its own presence. `presence` replaces your JSON object;
join/update/leave frames carry connection identities. Presence is ephemeral.

```json
{"t":"presence","data":{"cursor":{"x":30,"y":40}}}
{"t":"event","name":"player.jump","data":{"height":2}}
{"t":"event","name":"private-pose","data":{"x":1},"to":["CONNECTION_UUID"]}
{"t":"state","operation_id":"OPERATION_UUID","expected_version":3,"operations":[{"op":"set","key":"round","value":2},{"op":"increment","key":"score","by":1}]}
{"t":"sync"}
{"t":"ping"}
```

MCP also exposes the frame contract as `rogue://docs/realtime`.

State batches support `set`, `remove` and `increment`. Missing numeric keys start
at zero; nonnumeric/overflow increments reject the entire batch. Persisted changes
broadcast before the caller receives `ack`. Optional `expected_version` rejects
stale edits. The shared state is a key/value document, not a rich-text CRDT.

Use a UUID `operation_id`. The same subject and identical canonical input return
the saved receipt without applying the operation twice. Reusing that ID with a
different input is a conflict. Receipts expire after 10 minutes or eviction from
the most recent 256 operations per room. Beyond that bound, inspect application
state before deciding whether another write is safe. No exactly-once guarantee
extends past receipt retention. Ephemeral events have no receipts or replay.

In a `server` room, sockets can send presence/events but only the authenticated
project API can change durable state. Use events for player inputs and implement
validation, simulation, scoring and anti-cheat in your backend. A client pose
relay is appropriate for cooperative prototypes; it is not authoritative physics.

## REST, MCP and CLI parity

| REST path under `/api/v1` | Method | MCP tool / CLI |
| --- | --- | --- |
| `/realtime/capabilities` | GET | `get_realtime_capabilities` / `realtime capabilities` |
| `/realtime/protocol` | GET | Public frame contract |
| `/projects/{id}/realtime/rooms` | GET, POST | `list_realtime_rooms`, `create_realtime_room` / `list`, `create` |
| `/realtime/rooms/{id}` | GET, PATCH, DELETE | `get_realtime_room`, `update_realtime_room`, `delete_realtime_room` / `get`, `update`, `delete` |
| `/realtime/rooms/{id}/state` | GET, PATCH | `get_realtime_state`, `patch_realtime_state` / `state`, `patch` |
| `/realtime/rooms/{id}/events` | POST | `broadcast_realtime_event` / `event` |
| `/realtime/rooms/{id}/ticket` | POST | `create_realtime_ticket` / `ticket` |
| `/realtime/rooms/{id}/app-tickets` | POST | `create_realtime_app_ticket` / `app-ticket` |
| `/realtime/rooms/{id}/guest-tickets` | POST | Browser guest source; exact Origin required |
| `/realtime/rooms/{id}/publication-tickets` | POST | Trusted parent of an admitted Rogue app |
| `/realtime/rooms/{id}/connections/{connectionId}` | DELETE | `kick_realtime_connection` / `kick` |

`rog realtime watch ROOM_ID --duplex` prints newline-delimited server frames and
accepts newline-delimited JSON client frames on stdin. It renews its own ticket;
EOF or Ctrl-C closes it. Keep ticket output private. REST uses `{data,meta}`;
MCP returns the same data through its normal tool envelope.

## Limits and delivery

| Limit | Free | Pro |
| --- | ---: | ---: |
| Owned rooms | 10 | 100 |
| Connections per room | 16 | 128 |
| Durable JSON state | 256 KiB | 1 MiB |
| State keys | 256 | 2,048 |
| Application delivery per room/second | 1 MiB | 4 MiB |

Active admins have unlimited room counts, while per-room physical limits remain.
The owner plan is applied at admission and authenticated room operations.
Per connection: 30 frames/second with a burst of 60; 32 KiB incoming frames;
2 KiB presence; 16 KiB event/value; 32 state operations; 1 KiB identity info.
Snapshots and state acknowledgements are also metered against fan-out. Small
lease, heartbeat and connection lifecycle frames are additionally bounded by
connection/ticket limits. The browser SDK rejects sends once its outbound queue
exceeds 2 MiB (`queued_bytes`). Cloudflare manages the server's transport buffers;
Workers exposes no per-socket outbound buffer counter. The service enforces its
delivery rates and closes sockets on transport errors, without claiming a
separate server queue-size guarantee.

Frames are ordered on a live connection; state commits are serialized per room.
There is no cross-room ordering, offline event history, matchmaking, voice chat,
server physics or rich-text conflict resolution. Stored state survives room
hibernation and reconnects. Paused maintenance rejects new admission/control
writes; previously granted sockets remain writable until their short lease ends.
