# 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](https://docs.dev.board.fun/web/getting-started/build-and-deploy#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](https://docs.dev.board.fun/web/getting-started/build-and-deploy#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](https://docs.dev.board.fun/web/getting-started/build-and-deploy#3-deploy-to-a-device).

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

```javascript
// 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](https://docs.dev.board.fun/web/getting-started/build-and-deploy#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](https://docs.dev.board.fun/guides/touch-input).

## 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](https://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](https://docs.dev.board.fun/web/getting-started/build-and-deploy#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](https://docs.dev.board.fun/guides/) to wire up sessions, saves, and the pause overlay.
