Build app features
Use the generated application runtime as your normal integration point. Its lifecycle snapshot combines connection status, current state, resolved settings, and host capabilities. The complete data model explains individual fields; the API reference lists exported signatures and types.
Render the initial state and subsequent changes
import { defineApplication } from '@w3booster/sdk/app';
// Complete standalone demo. Registered apps import their generated w3boosterApp instead.
const w3boosterApp = defineApplication({
clientId: 'documentation_demo', revision: 'demo',
scopes: ['match:read', 'players:read'] as const,
settingsDefaults: {}
});
const runtime = w3boosterApp.createRuntime({ demo: true });
const output = document.createElement('p');
document.body.append(output);
runtime.lifecycle.subscribe(snapshot => {
output.textContent = snapshot.state?.players.map(player => player.name).join(' vs ')
|| (snapshot.status === 'connected' ? 'Waiting for players' : snapshot.status);
}, { signal: runtime.signal });
await runtime.start();
window.addEventListener('pagehide', () => { void runtime.stop(); }, { once: true });
The subscription runs immediately, including before match state exists. An already-running match is present in the initial snapshot; it does not synthesize a match.started event. Use the state to render and domain events for transitions such as a new hero or ended match.
During reconnect, retained state may still be visible while isSynchronized is false. Label stale data appropriately. Do not start another retry loop: the runtime already owns retries. Use snapshot.retry for progress, snapshot.error for connection failure, and structured issue events for nonfatal recorder or consumer problems.
Optional data is a normal state
Check state.capabilities before exposing a feature, and still check each optional field. A granted scope does not guarantee that the current source has a value. Show “Unavailable” when absence has meaning; do not turn unknown gold or health into a misleading zero.
Match data uses whole game-time seconds. Ability activation timing and upgrade timestamps use different documented units. Prefer SDK formatting and cooldown helpers. See the data model and standard-game helpers.
Read a finished match's result
With match:read, a finished match can include:
{
"id": "match-123",
"status": "finished",
"result": { "playerId": "0", "outcome": "won" }
}
This is a partial match example; normal snapshots also contain the other match fields.
| Field | Meaning |
|---|---|
match.result |
Optional recorder-confirmed outcome for the actual local player |
match.result.playerId |
That player's string ID within this match; can be joined to state.players with players:read |
match.result.outcome |
Exactly "won" or "lost" |
The result uses match:read on all plans; there is no extra result scope. It is
absent when unknown, in observer games, and in replays. It is not a complete list
of winners and losers, and it does not describe an observer's selected player.
An ended match without result is not evidence of a loss.
A result can arrive after the first finished snapshot and its match.ended
event. Subscribe to state updates to receive that later information. The platform
retains the current finished match until the next match starts, so hydration or
reconnection can deliver it again. This is the current terminal match, not a
historical results service. Use match.endedAt when present; lifecycle
observedAt is the client's observation time.
import type { W3BoosterClient } from '@w3booster/sdk';
export function displayMatchResult(
client: W3BoosterClient,
output: HTMLElement,
signal: AbortSignal
): () => void {
return client.state.subscribe(state => {
output.textContent = 'Result unavailable';
const match = state?.match;
if (match?.status !== 'finished' || !match.result) return;
const result = match.result;
output.textContent = `Match ${match.id}: player ${result.playerId} ${result.outcome}`;
}, { signal });
}
Call displayMatchResult(runtime.client, outputElement, runtime.signal) during
runtime setup. This example updates a display without incrementing a counter on
each state publication.
Apps own scoring policy. For a persistent counter, wait for all required player
facts, deduplicate by match.id, and save the processed ID together with the
counter in one atomic update. Multiple windows and retries must not increment
the same match twice. Excluding AI games or short losses, disabling automatic
updates, and resetting sessions are app decisions, not SDK behavior.
Settings: define, sync, read, write
Create settings in the W3Booster editor. Run npx w3booster-settings init YOUR_CLIENT_ID --endpoint https://api.w3booster.com once, then npm run w3booster:sync after definition changes. Commit the generated file. npm run w3booster:check performs a strict online comparison for connected CI; it is not an offline build check.
Read snapshot.settings for defaults merged with the user's saved values. Use runtime.client.host.setSetting(path, value) to save through a supported host, and await the result. Concurrent writes are serialized by the SDK. Avoid writing on every render; use explicit user actions or deliberate debouncing.
An APPLICATION_DEFINITION_MISMATCH means the deployed bundle carries an older definition. Regenerate, rebuild, and redeploy it. Normal builds use committed bindings; optional --install-hooks adds online refresh hooks. Plain JavaScript apps can generate --output src/w3booster.generated.js.
Host capabilities and OS access
Apps are sandboxed web pages. The SDK does not expose arbitrary filesystem paths, shell commands, processes, or Node.js modules. Browser features such as user-selected files and browser storage remain subject to browser rules. Browser storage is tied to the origin and browser profile; it is not shared application settings.
| Action | Embedded application | Detached application | Standalone demo / overlay |
|---|---|---|---|
| Open an app window | Discover window:open |
Discover window:open |
Do not assume support |
| Close this window | Embedded surface cannot close itself | Discover window:close |
Do not assume support |
| Save app settings | Discover settings:write |
Discover settings:write |
Do not assume support |
| Read match data | Granted scopes and available source | Same grant | Demo fixtures or authenticated overlay data |
Always use reactive capability discovery; available features depend on the actual host. client.host.command() is an application command channel, not a shell. The platform provides generic app storage; applications own their scoring and other feature logic. Do not assume that a host supports a command just because the SDK can send it; handle rejected actions.
Shared game context and app data
With SDK 2.0, every authorized app receives state.gameContext automatically. No scope or plan
is required, including for an app that requests no match data.
| Field | Meaning |
|---|---|
hudScale |
Warcraft HUD scale from 0.5 to 1.0; always present, defaults to 1 |
chatbarOpen |
Whether the game chat input is open; absent when unknown |
teamColors |
The team-color preference; absent when unknown |
The SDK's gameContext(state) selector supplies the same scale-1 fallback before
hydration. overlay:read and overlay.runtime are removed. Use gameContext
directly; every generated binding uses the current contract.
App-specific runtime data belongs in state.application.data, separately from
saved configuration in state.application.settings. Match Vision reads its
wins/losses counter from state.application.data.matchScore. Only Match Vision
receives that counter; there is no legacy score fallback. Adding overlay:read does not grant
another app access. Match Vision owns its counter mutations and automatic scoring rules; the platform
stores and transports app data without interpreting its fields.
Observer Economy needs only match:read, players:read, and resources:read.
Recorder discovery is transport metadata; the SDK hides recorder URLs and applies
updates under the granted data capabilities. Missing resource records remain
unavailable instead of becoming fabricated zeroes.
Observer/replay data can use a local recorder even with the production API. Browser permission to access that local service is separate from W3Booster data scopes.
Open a compact window
Use runtime.client.host.openWindow({ path: '?view=compact', width: 520, height: 620 }). The path is relative to your registered application URL. Await the host acknowledgement and show a useful error on failure. Use paths or query parameters for routing; authorization reserves the URL fragment.
Integrate your framework
Create a runtime inside the component or application lifetime, subscribe before calling start(), and call stop() at teardown. Catch startup rejection. Avoid a new runtime on every render. Framework examples are application code; the SDK remains framework-neutral.
- React: adapt
runtime.lifecycleusingcreateReactStoreanduseSyncExternalStore. Keep the runtime/store stable for one mounted lifetime. Create a new runtime for a new mount, including development Strict Mode remounts. The adapter defaults its server snapshot from the store; avoid constructing an incomplete lifecycle object. - Angular: use
createSubscribable(runtime.lifecycle)withtoSignal(), provideruntime.lifecycle.get()as the initial value, and stop through the owning destroy lifecycle. Match Vision's integration is a complete example. - Vue/Svelte: subscribe once in the mounted lifetime, assign the immutable snapshot to framework state, and unsubscribe/stop at unmount. SDK subscription methods return an unsubscribe function and support
AbortSignal. - Plain JavaScript/TypeScript: App Starter includes rendering, startup, page teardown, and hot-module cleanup.
Do not render private match data on a public server or persist credentials to solve hot reload. A full page/runtime reload may require reopening the app from the host for a fresh launch.