# Messages and manifest

The exact shapes OGS and a game exchange. The tables on this page are **generated from the zod
schemas** in [packages/ogs-protocol](https://github.com/open-game-system/open-game-system/blob/main/packages/ogs-protocol/src/frame.ts) when the site is
built, so they match the code. When anything here and the code disagree, the code wins.

With profile-kit you do not post or parse these by hand: see
[profile-kit reference](https://ogs-docs.pages.dev/profile-kit.md). This page is for games that do, for tests that fake the
launcher, and for agents that want the precise contract.

## Transport

- The launcher and the TV page talk with `window.postMessage`. Each message is a plain object with a
  `type` field.
- The launcher posts to your iframe's `contentWindow`. Your page posts to `window.parent` (profile-kit
  uses target origin `"*"`; the launcher checks the origin instead).
- The launcher accepts game messages only from the origin of the current game's TV URL, and drops
  anything that fails `GameToLauncherSchema`. An `ogs:instance` without a `title` labels nothing.
- Validate what you receive: ignore anything whose `event.source` is not `window.parent`, and parse
  with the schema (`LauncherToGameSchema.safeParse(event.data)`). Unknown messages are ignored, never
  errors.
- Every message is optional for the game. A game that answers nothing still runs.

## Launcher → game

`LauncherToGameSchema` in [frame.ts](https://github.com/open-game-system/open-game-system/blob/main/packages/ogs-protocol/src/frame.ts).

### `ogs:start`

The sitting starts or continues. Sent on the frame's load, and again after each `ogs:ready`.

| Field | Type | Required | Description |
|---|---|---|---|
| `instanceId` | string | yes | The OGS sitting this frame is for. |
| `mode` | "continue" \| "new" | yes | `continue` an earlier sitting, or start a `new` one. |
| `roster` | [RosterEntry](#rosterentry)[] | yes | Who sits where: profile, role and device, when the couch picked roles. |
| `token` | string | yes | A game token for this game and session (aud = appId, players + sid); "" when there is none. |
| `players` | [GamePlayer](#gameplayer)[] | no | Who's on the couch: profile id, @id, name, avatar. |
| `room` | [RoomId](#roomid) | no | Join this room (another couch made it) instead of making one (multiCouch games). |

### `ogs:suspend`

The launcher parked the frame (Home, or another game). It stays loaded: go silent.

No payload: `{ type: "ogs:suspend" }`.

### `ogs:resume`

Continue: the same parked frame is shown again, no reload. Resume sound.

No payload: `{ type: "ogs:resume" }`.

## Game → launcher

`GameToLauncherSchema` in [frame.ts](https://github.com/open-game-system/open-game-system/blob/main/packages/ogs-protocol/src/frame.ts).

### `ogs:ready`

The page is listening; the launcher re-sends `ogs:start` for the current sitting.

No payload: `{ type: "ogs:ready" }`.

### `ogs:resume-point`

The sitting's label (older form of `ogs:instance`).

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string | yes | For example `"Mission 6"`. |

### `ogs:room`

The room the TV page shows (multiCouch games): the couch session keeps it on the sitting.

| Field | Type | Required | Description |
|---|---|---|---|
| `room` | [RoomId](#roomid) | yes | The game's own room code. |

### `ogs:instance`

The sitting's report; the launcher uses `report.title` as its label.

| Field | Type | Required | Description |
|---|---|---|---|
| `report` | [InstanceReport](#instancereport) | yes | What the game says about this sitting. |

## Phone page → app

In the OGS app's WebView a game page talks to the app over the app bridge, not `postMessage` to a
parent. profile-kit uses two bridge stores:

| Store | Direction | Content |
|---|---|---|
| `profile` | app → page | `{ status: "asking" }`, `{ status: "ready", profile: OgsProfile }` or `{ status: "none" }` |
| `ogs` | page → app | event `{ type: "INSTANCE_REPORT", report: InstanceReport }` (`OgsBridgeEventSchema`) |

Room games declare their TV page with cast-kit's `useCastViewUrl(url)`; the app forwards it to the
launcher. See [the contract, §3](https://ogs-docs.pages.dev/contract.md#3-phones-the-game-page-in-the-apps-webview).

## A typical sequence

```
launcher                                   your TV page (iframe)
   |  load iframe (allow="autoplay; fullscreen")  |
   |--- ogs:start {instanceId, mode, roster, token, players} --->|
   |<-------------------------- ogs:ready ---------------------- |  (profile-kit, on start-up)
   |--- ogs:start (again, for the current sitting) ------------> |
   |<---------- ogs:instance {report: {title: "Mission 1"}} ---- |
   |              ... the couch plays ...                        |
   |--- ogs:suspend -------------------------------------------> |  Home: go silent, stay loaded
   |--- ogs:resume --------------------------------------------> |  Continue: sound back, no reload
```

## Manifest

A game is config, not code: one `Manifest` (`ManifestSchema` in
[manifest.ts](https://github.com/open-game-system/open-game-system/blob/main/packages/ogs-protocol/src/manifest.ts)). Fields with a default may be left out.

| Field | Type | Required | Description |
|---|---|---|---|
| `appId` | string (`^[a-z0-9-]+$`) | yes | The game's id; also the `aud` of every game token for this game. |
| `name` | string (non-empty) | yes | Shown in the Library and on the TV. |
| `tagline` | string | no (default `""`) | One line under the name in the Library. |
| `shape` | "couch" \| "live" \| "async" | yes | `couch`: played together in one room; `live`: real-time online; `async`: turns over days. |
| `tv` | "none" \| "optional" \| "required" | yes | Whether the game needs a TV. |
| `startUrl` | string (URL) | yes | Played on a phone or tablet (also the controller URL when the TV shows tvUrl). |
| `tvUrl` | string (URL) | no | A static TV page. Room-based games omit it: their phone page sends the room's TV URL at runtime (cast-kit useCastViewUrl). |
| `roles` | [Role](#role)[] | no (default `[]`) | Who plays: each role's `audience` lets OGS suggest who sits where. |
| `art` | object | yes | The art kit (see Art kit and catalogue). |
| `art.tile` | string (non-empty) | yes | Older captured TV screenshot, 16:9 (required). |
| `art.hero` | string | no | Older captured hero image, 16:9. |
| `art.icon` | string (non-empty) | no | 1:1 icon. |
| `art.cover` | string (non-empty) | no | 2:3 cover with the title. |
| `art.logo` | string (non-empty) | no | Transparent logo. |
| `art.heroClean` | string (non-empty) | no | 16:9 hero with no text and no HUD. |
| `art.theme` | string (non-empty) | no | The game's music theme: a 20–40 s seamless audio loop (music only) the launcher's Home plays while the game is focused. |
| `art.safe` | object | no | Crops a HUD out of `tile`/`hero` when there is no clean art: zoom about a point. |
| `art.safe.scale` | number (> 0) | yes | Zoom factor. |
| `art.safe.ox` | number | yes | Zoom origin x, percent of the width. |
| `art.safe.oy` | number | yes | Zoom origin y, percent of the height. |
| `shop` | object | no (default `{}`) | Shown on the game's page in the app. |
| `shop.ages` | string | no | For example `"4+"`. |
| `shop.minutes` | [number, number] | no | Typical sitting length, `[min, max]` minutes. |
| `shop.players` | string | no | For example `"2-4"`. |
| `multiCouch` | boolean | no | Several couches may join one room of this game (spec §7). Absent = single couch. |
| `instanceTtlMs` | number (> 0) | no (default `604800000`) | How long an instance may stay silent before it expires (ms). |

An example:

```json
{
  "appId": "space-bakery",
  "name": "Space Bakery",
  "tagline": "Bake for aliens, together.",
  "shape": "couch",
  "tv": "required",
  "startUrl": "https://space-bakery.example.com/",
  "roles": [
    { "id": "chef", "label": "Chef", "audience": "grownup" },
    { "id": "helper", "label": "Helper", "audience": "kid" }
  ],
  "art": {
    "icon": "/art/space-bakery/icon.png",
    "cover": "/art/space-bakery/cover.jpg",
    "logo": "/art/space-bakery/logo.png",
    "heroClean": "/art/space-bakery/hero-clean.jpg",
    "tile": "/art/space-bakery/tv.jpg"
  },
  "shop": { "ages": "4+", "minutes": [10, 20], "players": "2-4" }
}
```

No `tvUrl`: this is a room game whose phone page declares the room's TV page at runtime. A game with
one TV page for everyone adds `"tvUrl": "https://space-bakery.example.com/tv"`.

## Shared types

### `GamePlayer`

Someone on the couch (`ogs:start.players`, `GameToken.players`).

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (non-empty) | yes | OGS profile id. |
| `handle` | string (non-empty) | yes | The profile's @handle. |
| `name` | string (non-empty) | yes | Display name. |
| `avatar` | string (URL) | yes | Avatar image URL. |

### `RosterEntry`

Who sits where (`ogs:start.roster`).

| Field | Type | Required | Description |
|---|---|---|---|
| `profileId` | string | yes | OGS profile id. |
| `roleId` | string | yes | One of the manifest's `roles[].id`. |
| `deviceId` | string | no | The device this player plays on. |

### `InstanceReport`

What a game reports about a sitting (`ogs:instance`, `reportOgsSitting`). Only `instanceId`, `appId` and `status` are needed.

| Field | Type | Required | Description |
|---|---|---|---|
| `instanceId` | string (non-empty) | yes | The sitting's id. |
| `appId` | string (non-empty) | yes | The game's appId. |
| `status` | [InstanceStatus](#instancestatus) | yes | Where the sitting is. |
| `title` | string | no (default `""`) | The sitting's label, for example `"Mission 6"` or `"Room KQTP"`. |
| `detail` | string | no (default `""`) | A second line under the label. |
| `yourTurn` | boolean | no | For async games: true when it's this profile's move. |
| `startsAt` | number | no | For scheduled sittings (a game night): when it starts, ms since epoch. |
| `resumeUrl` | string (URL) | no | Where to send people back in. |

### `InstanceStatus`

One of `lobby`, `active`, `suspended`, `waiting`, `completed`, `expired`.

### `GameToken`

The claims of a game token, as `verifyOgsToken` returns them.

| Field | Type | Required | Description |
|---|---|---|---|
| `iss` | string (non-empty) | yes | The OGS API that signed it. |
| `aud` | string (non-empty) | yes | The game (appId) this token is for. |
| `sub` | string (non-empty) | yes | OGS profile id (a session token: the host's). |
| `handle` | string (non-empty) | yes | The profile's @handle. |
| `name` | string (non-empty) | yes | Display name. |
| `avatar` | string (URL) | yes | Avatar image URL. |
| `sid` | string (non-empty) | no | The couch session (TV tokens only). |
| `players` | [GamePlayer](#gameplayer)[] | no | Who's on the couch (TV tokens only). |
| `couch` | [CouchClaim](#couchclaim) | no | The couch this token was issued for (TV tokens, and phones that asked with their session). |
| `iat` | number (integer, ≥ 0) | yes | Issued at, seconds since epoch. |
| `exp` | number (integer, > 0) | yes | Expiry, seconds since epoch (1 hour after `iat`). |

### `CouchClaim`

The couch a token was issued for.

| Field | Type | Required | Description |
|---|---|---|---|
| `sid` | string (non-empty) | yes | The couch session id. Players with the same `sid` sit on the same couch. |
| `label` | string (non-empty) | yes | The couch's label (the host's name). |

### `Role`

One of the manifest's roles.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (non-empty) | yes | The role's id (`RosterEntry.roleId`). |
| `label` | string (non-empty) | yes | Shown to people, for example `"Captain"`. |
| `audience` | "grownup" \| "kid" \| "little" | yes | Who the role suits: a grown-up, a kid, or a little one. |

### `RoomId`

A string matching `^[A-Za-z0-9_-]{1,64}$`: the game's own room code.
