Build & Deploy

The full dev loop for a web game: build it, pack it, and run it on a device. The flow is cross-platform (macOS, Linux, Windows) and works the same for a person at a keyboard or an automated agent.

Prerequisites

  • A Board device on the MP.1.9.x OS family or newer. The device must expose the web-app install and logs capabilities; confirm with board-connect capabilities (see Confirm device compatibility).
  • Node 18 or newer for the build toolchain.
  • The web-pack and board-connect tools available on your machine.
  • A Piece Set Model (model.tflite) from the developer portal — every web app bundles one (see Every app needs a model).

1. Build the web app

Projects scaffolded with npm create @board.fun/game come with all of this pre-configured — relative base path, pack script, and model placeholder — so you can skip straight to deploying.

Produce a static build with relative asset paths so it loads correctly when served from the device. With Vite, set the base path to './':

// vite.config.js
export default {
  base: "./",
};
vite build

This writes your app to dist/. Any bundler works as long as it emits a static build with relative paths.

Relative paths are not optional. Most bundlers default to site-root URLs (src="/assets/index.js"), which assume the app is served from the root of a website. On a Board the app is served from a folder, so those URLs resolve to nothing — the result is a white screen with no console output, because the app’s JavaScript never loads. Check dist/index.html: every src/href should start with ./, not /. (web-pack refuses to pack a build with root-absolute references; in Vite the fix is base: "./", in webpack output.publicPath: "./".)

2. Pack the app

Use web-pack to turn the build into an installable Board web app:

web-pack dist --package-id fun.board.mygame --name "My Game"

web-pack writes a harness-config v1 manifest and a single flat <appId>.webapp.zip. It uses a pure-JS zip implementation, so it produces identical archives on Windows, macOS, and Linux.

This expects model.tflite in dist/ — every web app bundles a Piece Set Model (see Every app needs a model); pass --model <path> if it lives elsewhere.

App identity: packageId, appId, and board.config.json

The manifest carries two identifiers, mirroring an APK:

  • packageId is a reverse-domain key (e.g. fun.board.mygame) and the app’s canonical identity. You supply it via --package-id, or via packageId in an existing harness-config.json or board.config.json. It is required, and it is never derived from the filesystem path, so building from a different checkout location produces the same bundle identity.
  • appId is a canonical UUID the device scopes saves and profiles by. The first time you run web-pack, it mints a random UUID and persists it to board.config.json in your project. The appId is deliberately not derived from the package id: the device namespaces each app’s save directory by appId, so a predictable id would let anyone compute (and overwrite) another app’s saves. Pass --app-id <uuid> only to adopt a pre-existing id.

Commit board.config.json to source control. The appId ties your app to its saved games on the device. If the appId changes, the device treats the app as a different app and the old saves are no longer associated with it. Keeping board.config.json in the repo means rebuilds reuse the same appId and saves survive.

What web-pack validates

web-pack runs the checks the device’s install gate enforces, so a bad bundle fails on your machine instead of after an upload:

  1. harness-config.json present (generated by web-pack).
  2. schemaVersion equals 1.
  3. packageId is a valid reverse-domain id.
  4. appId is a canonical UUID (random, minted once and persisted to board.config.json).
  5. name is present (1-64 chars).
  6. sdkVersion is semver (stamped from the installed @board.fun/web-sdk).
  7. The entry file (default index.html) exists in the bundle and is .html/.htm.
  8. The entry HTML has no root-absolute asset URLs (src="/assets/..."): the device serves the app from a folder, not a site root. Use a relative base path (Vite: base: "./").
  9. model is present: a bundle-relative file path that exists (its sha256 is recorded). Every Board web app bundles a touch model.
  10. entry/model/icon resolve inside the bundle root (no ../absolute-path escapes).
  11. Some .js/.html references a Board SDK global (window.BoardSDK / window.Harness / window.__board / window.boardTouch), i.e. the bundle was built with @board.fun/web-sdk.

Run web-pack -h for the full list of options.

Every app needs a model

Every Board web app bundles a Piece Set Model. The on-device touch detector is what delivers contacts — finger and Piece — to Board.input, and it only runs when the bundle ships a model. A bundle without one installs and launches, but the device disables Board input entirely: subscribe connects and then no frames ever arrive. web-pack refuses to pack a bundle with no model for exactly this reason.

The model is obtained out of band:

  1. Download model.tflite for your game’s Piece Set from the developer portal.
  2. Keep it in your build output — e.g. public/model.tflite in a Vite project, so the bundler copies it into dist/ — and web-pack picks it up automatically. Or keep it elsewhere and pass it explicitly:
web-pack dist --package-id fun.board.mygame --name "My Game" --model ./model.tflite

web-pack records the model’s sha256 in the manifest. The model is never bundled automatically by the tooling and never downloaded at runtime.

If your game doesn’t use Pieces, bundle a model anyway: finger input runs through the same detector, so any Piece Set Model enables it — the Glyph recognition simply goes unused. See Input & Pieces.

Confirm device compatibility

Check that the target device exposes the capabilities this workflow needs:

board-connect capabilities

Look for the web-app install and logs capabilities. The device must be on the MP.1.9.x family or newer for these to be present.

3. Deploy to a device

Pair with the device once so later commands need no address (you tap Approve on the device):

board-connect pair <host>

Install the packed app with board-connect and launch it:

board-connect install <appId>.webapp.zip --launch

Watch its output while it runs (add --follow to stream live until Ctrl-C):

board-connect logs <appId>

Grab a screenshot of the current screen:

board-connect screenshot --out shot.png

board-connect is the supported cross-platform path and is the right tool for an automated agent driving the dev loop. Download it from the developer portal at dev.board.fun.

Or use the Board Connect web UI

The same install works without the CLI: open the Board’s address in a browser (shown on the device under Settings > System, along with the pairing screen), pair, and drop the .webapp.zip onto the Apps tab.

The dev loop

Change code, vite build, web-pack, board-connect install, watch board-connect logs, repeat.

Device note: first install reports host unavailable

On a cold device, the in-device browser host can sit in a restricted standby bucket, and the first install may report that the host is unavailable. If that happens, foreground the Board Browser on the device once to wake it, then retry the install. Subsequent installs work without this step.

Troubleshooting

White screen after launch, and board-connect logs shows nothing. Your build references its assets with root-absolute URLs, so the app’s JavaScript never loads (and code that never runs logs nothing). Open dist/index.html: if any src/href starts with /, set your bundler’s base path to ./ (Vite: base: "./"; webpack: output.publicPath: "./") and rebuild. Current web-pack versions catch this at pack time.

No frames from Board.input.subscribe (fingers or Pieces). The installed bundle has no Piece Set Model, so the device launched it with Board input disabled. See Every app needs a model, then re-pack and re-install.

board-connect logs reports an error or shows nothing. The id is the app’s appId UUID (in board.config.json, printed by web-pack, and listed by board-connect apps) — not your package id. logs takes the id as its only argument; the target Board comes from pairing (-b <ip> to override).

Next

Once your game runs on a device, revisit the Guides to wire up sessions, saves, and the pause overlay.