AI Assistant
This page does two things. First, it tells you how to set up Claude, Cursor, GitHub Copilot, or any other AI coding assistant so it can write correct, idiomatic Board Web SDK code. Second, the Quick Reference section below IS the payload you can paste directly into your tool of choice as a system prompt, a rules file, or a top-of-file comment block. Copy-paste-and-go.
Note: Everything here is optional. The SDK works the same with or without AI assistance. If you would rather hand-roll, the API Reference and the FAQ have every signature and gotcha you need.
Setup
Claude
Claude reads any CLAUDE.md files in or above the directory it is working in. Drop the Quick Reference below into a CLAUDE.md at your project root and Claude will follow the conventions for free.
Cursor
Cursor reads a rules file at your project root and injects it into every chat. Copy the entire Quick Reference section below into that file, commit it to source control, and the whole team gets the same context.
GitHub Copilot and others
Copilot does not have a project-level rules file. The next-best thing is a top-of-file comment block in your main module that contains the Quick Reference below; Copilot reads surrounding file context aggressively and picks up the conventions. For ChatGPT, Aider, or any tool with a system-prompt slot, paste the Quick Reference in directly.
Quick Reference
(This is the section to copy into your AI tool. Everything below this line is written as a self-contained briefing for an AI assistant.)
You are writing TypeScript against the Board Web SDK (@board.fun/web-sdk). Board is a 23.8” 1080p landscape touch console; its touch sensor detects both fingers and physical Pieces (game tokens with conductive Glyph patterns on their bases). The SDK is ESM-only and runs your web app inside Board’s built-in browser.
Basics
- Import the single frozen
Boardobject:import { Board, BoardContactType } from "@board.fun/web-sdk";. Every feature hangs off it across six domains:Board.input,Board.session,Board.save,Board.avatar,Board.pause,Board.application. - Always gate device calls on
Board.isOnDevice. In a desktop browser it isfalseand service-backed calls do NOT no-op: sync calls (Board.session.*,Board.pause.*,Board.application.*, thesavegetters) throw, and async calls (Board.save.*,Board.avatar.*,Board.session.present*) reject because the native bridge is absent. The one exception isBoard.input.getContacts(), which safely returns[]. Gate sync calls behind anif (Board.isOnDevice)check and wrap async calls intry/catchso the same build stays runnable without hardware. - Do not gate features on
Board.sdkVersion. It is informational only. There is no bridge or OS version exposed to games; new OS capabilities degrade gracefully via in-bridge capability checks. The runtime gate isBoard.session.areServicesReady(). - Two async styles: Promises for request/response calls, and callbacks/subscriptions for the touch stream and pause results.
Touch input
Subscribe to Board.input; each callback receives a full per-frame snapshot of contacts. Filter Pieces by glyphId, not by the contact type.
function onContacts(contacts: BoardContact[]) {
for (const c of contacts) {
if (c.type === BoardContactType.Glyph) {
handlePiece(c.glyphId, c.x, c.y, c.orientation);
}
}
}
if (Board.isOnDevice) {
Board.input.subscribe(onContacts);
}
Each contact: contactId, type (BoardContactType.Finger / Glyph / Blob), glyphId, x, y (device pixels, origin top-left, Y down), orientation, phase. There are no discrete down/up events; keep your own previous-frame map keyed by contactId and diff it for edges. All contacts — finger and Piece — come from the on-device detector, which only runs when the bundle ships a Piece Set Model (recorded at pack time): a bundle with no model gets no input frames at all.
Players and sessions
The roster is OS-owned; the game never silently adds or removes players. Read with getPlayers() / getPlayerCount() / getActiveProfile(). Change it through the OS selector (presentAddPlayer(aiTypeIndices?) / presentReplacePlayer(sessionId, aiTypeIndices?), both resolve Promise<boolean>, true = added/replaced, false = dismissed) or resetPlayers(). Declare AI types with setAIPlayerTypes([{ name, description }]).
const added = await Board.session.presentAddPlayer();
if (added) refreshRoster();
Save games
Create, load, list, and update, all Promise-based: create, load, list, update. There is no direct delete. A game removes its own involvement with removePlayersFromSave (the current game’s players) or removeActiveProfileFromSave (only the active profile); the system deletes the save once no players remain. Plus loadCoverImage, getAppStorageInfo(), and getUniquePlayers(saves).
const meta = await Board.save.create("Turn 12", payload /* Uint8Array */, playedTimeMs, gameVersion);
const saves = await Board.save.list();
await Board.save.removePlayersFromSave(meta.id);
// or, to remove only the active profile:
await Board.save.removeActiveProfileFromSave(meta.id);
Save metadata: id, description, createdAt, updatedAt, playedTime, fileSize, gameVersion, playerCount, players[] (each playerId, name, avatarId, type, aiTypeIndex), hasCoverImage, payloadChecksum, coverImageChecksum.
Avatars
Board.avatar.loadPNG(avatarId) resolves to a cached PNG data URI; getDefault() is avatar 0; forPlayer(player) is a shortcut. Assign the data URI straight to an <img> element’s src.
Pause overlay
The OS owns the menu button and UI; the game supplies context and reads results. setContext(context), updateContext(partial), clearContext(). Subscribe with onResult(callback) (preferred); a legacy pollResult() exists.
Board.pause.setContext({
offerSaveOption: true, // adds the system "Save & Quit" option
customButtons: [{ id: "restart", title: "Restart", icon: "circulararrow" }],
});
Board.pause.onResult((result) => {
if (result.action === "quit" || result.action === "save_and_quit") Board.application.quit();
if (result.action === "custom_button" && result.customButtonId === "restart") restartGame();
});
Application lifecycle
Board.application.quit() closes the web app and returns to the launcher. showProfileSwitcher() / hideProfileSwitcher() drive the OS profile switcher.
Build and deploy
For a new project, scaffold with npm create @board.fun/game my-game (add -- --template showcase for a reference app exercising every SDK domain) — it pre-wires the base path, pack scripts, and model placeholder. Otherwise: build a static app with relative paths (base: "./" in Vite). Pack with web-pack dist --package-id fun.board.mygame --name "My Game", which writes a flat <appId>.webapp.zip and mints a random-UUID appId persisted to board.config.json (commit that file so saves survive rebuilds). Every bundle ships a Piece Set Model (required — finger input needs it too): download it out of band from the developer portal, keep it in your build output (e.g. public/model.tflite) so web-pack picks it up, or pass --model <path>; the tooling never vendors or downloads it at runtime. Pair once with board-connect pair <ip> (tap Approve on the device), which saves that Board as the default so later commands need no address. Then deploy with board-connect install <appId>.webapp.zip --launch, watch board-connect logs <appId> --follow, and confirm device support with board-connect capabilities (MP.1.9.x family or newer). If the first install reports the host unavailable, foreground the Board Browser once and retry.
Important notes
- Gate on
Board.isOnDevice, not onBoard.sdkVersion. The runtime capability gate isBoard.session.areServicesReady(). - Y-down coordinates, no flip. Contacts are device pixels, origin top-left.
- Identify Pieces by
glyphId, not by the contact type. - There is no direct save delete. A game removes its own players with
removePlayersFromSaveorremoveActiveProfileFromSave. The system deletes the save automatically once no players remain associated with it. A game cannot directly delete a shared save, which protects other players’ progress. This matches the Unity SDK. - Commit
board.config.jsonso theappId(and therefore saved games) survives rebuilds.
For more depth see the API Reference and the Guides. File feedback at hello@board.fun.