# Web API Reference

The complete `Board` object surface. `Board` is a compile-time readonly object (`export const Board = {...} as const`); every feature hangs off it, grouped into six domains plus top-level properties.

```
import { Board, BoardContactType } from "@board.fun/web-sdk";

Board.isOnDevice; // boolean, true on a real device
Board.sdkVersion; // string, informational only, NOT a feature gate
```

> The public `Board.*` API is append-only: nothing is removed or renamed once shipped, so a game built against an older SDK keeps working on newer devices. There is intentionally no bridge or OS version exposed to games. New OS capabilities degrade gracefully via in-bridge capability checks; where needed, gate on `Board.session.areServicesReady()`.

The SDK also exposes the same surface as direct named ES exports for callers who prefer named imports over `Board.*`: the domain objects `input`, `session`, `save`, `avatar`, `pause`, `application`; `isBoardDevice()`; `SDK_VERSION`; and the `TouchFrameCallback` type (alongside the `Board*` enums and interfaces).

## Top-level

| Member | Type | Description |
| --- | --- | --- |
| `Board.isOnDevice` | `boolean` | `true` when running on Board hardware. Gate every device call on this. |
| `Board.sdkVersion` | `string` | SDK version string. Informational only. |

## Async style

The SDK uses two async styles:

- **Promises** for request and response calls, anything that fetches or mutates state (`Board.save.*`, `Board.avatar.*`, `Board.session.presentAddPlayer()`, and so on) returns a `Promise`.
- **Callbacks and subscriptions** for streams, touch contacts (`Board.input.subscribe`) and pause results (`Board.pause.onResult`) are delivered to a callback you register.

## Board.input

| Member | Description |
| --- | --- |
| `Board.input.subscribe(callback)` | Start receiving a per-frame snapshot of all active contacts. |
| `Board.input.unsubscribe(callback)` | Stop receiving snapshots. |
| `Board.input.getContacts()` | Snapshot of the current active contacts as a `ReadonlyArray<BoardContact>`. |
| `Board.input.getContactsByType(type)` | Same snapshot filtered to a `BoardContactType`, as a `ReadonlyArray<BoardContact>`. |
| `Board.input.isSubscribed` | `boolean` getter, whether the push channel is active. |
| `BoardContactType` | Enum, `Finger`, `Glyph`, `Blob`. |
| `BoardContactPhase` | Enum, `None`, `Began`, `Moved`, `Ended`, `Canceled`, `Stationary`. |

Each contact in a snapshot carries: `contactId`, `type`, `glyphId`, `x`, `y`, `orientation`, `phase`, `isTouched`. Positions are device pixels, origin top-left, Y down. `orientation` is in degrees and is only meaningful for Glyph contacts. `phase` is the contact’s lifecycle stage; a still Piece reappears every frame with `phase``Stationary`. `isTouched` is `true` while a hand is physically touching the Piece. Identify Pieces by `glyphId`, not by the type.

## Board.session

| Member | Description |
| --- | --- |
| `Board.session.getPlayers()` | The current roster. |
| `Board.session.getPlayerCount()` | Number of players. |
| `Board.session.addGuest(sessionId)` | Add a guest player with a session-unique id. |
| `Board.session.removePlayer(sessionId)` | Remove a player. |
| `Board.session.resetPlayers()` | Clear to initial state. |
| `Board.session.getActiveProfile()` | The OS active profile, or null. |
| `Board.session.presentAddPlayer(aiTypeIndices?)` | Open the selector to add. Resolves `Promise<boolean>`. |
| `Board.session.presentReplacePlayer(sessionId, aiTypeIndices?)` | Open the selector to replace. Resolves `Promise<boolean>`. |
| `Board.session.setAIPlayerTypes(types)` | Declare AI types (`{ name, description }[]`) for the selector. |
| `Board.session.isReady()` | Whether the session manager is ready. |
| `Board.session.areServicesReady()` | Whether device services are up. |
| `BoardPlayerType` | Enum (string-valued), `Profile` (`"profile"`), `Guest` (`"guest"`), `AI` (`"ai"`). The type of `BoardPlayer.type` and `BoardSaveGamePlayer.type`. |

The roster is OS-owned. Games never silently add or remove players; changes go through the selector or `resetPlayers()`.

`areServicesReady()` may return `false` briefly right after launch while the bridge connects to device services, then become `true`. Treat it as a live check before session and save calls, not a one-time probe at startup.

`BoardPlayer` (returned by `getPlayers()` and `getActiveProfile()`):

| Field | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Persistent app-specific player ID. |
| `sessionId` | `number` | Session-unique ID. |
| `name` | `string` | Display name. |
| `type` | `BoardPlayerType` | Whether this is a profile, guest, or AI player. |
| `avatarId` | `string` | Avatar identifier. |
| `aiTypeIndex` | `number?` | AI personality index when `type` is `AI`. Absent for human players. |

## Board.save

| Member | Description |
| --- | --- |
| `Board.save.create(description, data, playedTime, gameVersion)` | Create a save. Resolves the `BoardSaveGameMetadata`. |
| `Board.save.load(saveId)` | Load a save, including payload. |
| `Board.save.list()` | List saves (metadata). |
| `Board.save.update(saveId, description, data, playedTime, gameVersion)` | Update a save. Resolves `void`. |
| `Board.save.removePlayersFromSave(saveId)` | Remove the current game’s players from a save. The system deletes the save once no players remain. |
| `Board.save.removeActiveProfileFromSave(saveId)` | Remove only the active profile from a save. The system deletes the save once no players remain. |
| `Board.save.loadCoverImage(saveId)` | Load a save’s cover image. |
| `Board.save.getMaxDataSize()` | Max payload size in bytes for a single save. |
| `Board.save.getMaxAppStorageSize()` | Max total storage in bytes across all saves. |
| `Board.save.getMaxDescriptionLength()` | Max description length. |
| `Board.save.getAppStorageInfo()` | Storage totals and usage percentage. |
| `Board.save.getUniquePlayers(saves)` | Unique profile players across saves. |

Save metadata: `id`, `description`, `createdAt`, `updatedAt`, `playedTime`, `fileSize`, `gameVersion`, `playerCount`, `players` (each `playerId`, `name`, `avatarId`, `type`, `aiTypeIndex`), `hasCoverImage`, `payloadChecksum`, `coverImageChecksum`.

`BoardAppStorageInfo` (returned by `getAppStorageInfo()`):

| Field | Type | Description |
| --- | --- | --- |
| `totalStorage` | `number` | Total storage available to the app, in bytes. |
| `usedStorage` | `number` | Storage currently used by the app, in bytes. |
| `remainingStorage` | `number` | Storage still available to the app, in bytes. |
| `usagePercentage` | `number` | Fraction of storage used, from `0.0` to `1.0`. |

## Board.avatar

| Member | Description |
| --- | --- |
| `Board.avatar.loadPNG(avatarId)` | Load an avatar image as a PNG data URI. Cached by `avatarId`. |
| `Board.avatar.getDefault()` | The default avatar (0) as a data URI. |
| `Board.avatar.forPlayer(player)` | Load a player’s avatar as a data URI. |
| `Board.avatar.clearCache()` | Drop the avatar cache. |

## Board.pause

| Member | Description |
| --- | --- |
| `Board.pause.setContext(context)` | Set the pause-menu context. |
| `Board.pause.updateContext(partial)` | Merge a partial update. |
| `Board.pause.clearContext()` | Clear the context. |
| `Board.pause.onResult(callback)` | Subscribe to pause results. Preferred. |
| `Board.pause.pollResult()` | Legacy, poll the last result. |

The OS owns the menu button and UI. The game supplies context and reads results.

`BoardPauseContext` (passed to `setContext()`; `updateContext()` takes a partial):

| Field | Type | Description |
| --- | --- | --- |
| `gameId` | `string?` | App/game ID. Used by the OS pause overlay to attribute the session. |
| `gameName` | `string?` | Display name of the game, shown in the pause menu header. |
| `offerSaveOption` | `boolean?` | Whether the pause menu shows a “Save & Quit” option in addition to “Quit”. |
| `customButtons` | `BoardPauseButton[]?` | Custom buttons appended to the pause menu. |
| `audioTracks` | `BoardPauseAudioTrack[]?` | Audio track volume sliders shown in the pause menu. |

`BoardPauseButton`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | Stable identifier for this button. Returned as `customButtonId` on `BoardPauseResult` when pressed. |
| `title` | `string` | Text shown on the button. |
| `icon` | `'circulararrow' \| 'doorwitharrow' \| 'leftarrow' \| 'square' \| ''` | Icon name; the empty string means no icon. |

`BoardPauseAudioTrack`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | Stable identifier for this audio track. Returned on `BoardPauseResult.audioTracks` when the user adjusts the slider. |
| `name` | `string` | Display name shown next to the volume slider. |
| `value` | `number` | Volume value, `0`-`100`. |

`BoardPauseResult` (delivered to `onResult()`; also returned by `pollResult()`):

| Field | Type | Description |
| --- | --- | --- |
| `action` | `'resume' \| 'quit' \| 'save_and_quit' \| 'custom_button'` | Action taken. For a custom button this is `'custom_button'` and the specific button is in `customButtonId`. |
| `customButtonId` | `string?` | ID of the custom button that was pressed, when `action` is `'custom_button'`. |
| `audioTracks` | `BoardPauseAudioTrackResult[]?` | Updated audio track values, if the user adjusted any sliders before dismissing the menu. |

`BoardPauseAudioTrackResult`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | ID of the audio track (matches `BoardPauseAudioTrack.id`). |
| `value` | `number` | New volume value, `0`-`100`, after the user adjusted the slider. |

## Board.application

| Member | Description |
| --- | --- |
| `Board.application.quit()` | Close the web app, return to the launcher. |
| `Board.application.showProfileSwitcher()` | Open the OS profile switcher. |
| `Board.application.hideProfileSwitcher()` | Close the OS profile switcher.
