Appearance
Host integration
A host mounts a game, supplies environment settings, starts and pauses runs, and displays results. The game remains responsible for its scene and rules. Use the SDK's messages instead of reaching into game internals. For an existing game engine, use the game-side createBridge API to connect its lifecycle and loop to these host commands.

Iframe handshake
Serve the host and game from the same origin. The game sends hello to its parent with pp: 2, identity, build, SDK version and capabilities. Check both event.origin and event.source against the expected game frame. Create a MessageChannel, transfer one port with init, and use the other port for subsequent commands and messages.
This minimal harness assumes an existing <iframe id="game"> and two buttons, start and dispose. Install the listener before assigning the frame URL. It demonstrates one game document; a feed also needs viewport/layout handling, user-gesture forwarding and session audio ownership described below.
js
const frame = document.querySelector('#game');
let port;
let seq = 0;
let disposeSeq;
const send = (t, payload = {}) => port.postMessage({ t, seq: ++seq, ...payload });
window.addEventListener('message', event => {
if (event.origin !== location.origin || event.source !== frame.contentWindow) return;
if (event.data?.pp !== 2 || event.data?.t !== 'hello' || port) return;
const channel = new MessageChannel();
port = channel.port1;
port.onmessage = ({ data }) => {
if (data.t === 'ready') document.querySelector('#start').disabled = false;
if (data.t === 'over') console.log('Final score:', data.score);
if (data.t === 'ack' && data.seq === disposeSeq && data.s === 'disposed') {
port.close();
frame.src = 'about:blank';
}
};
frame.contentWindow.postMessage({
pp: 2, t: 'init', seq: ++seq, v: 2,
muted: true, insets: { top: 0, right: 0, bottom: 0, left: 0 },
tier: 'mid', dprCap: 2, reducedMotion: false,
best: null, seed: 42, test: false, container: 'browser',
}, location.origin, [channel.port2]);
});
document.querySelector('#start').disabled = true;
document.querySelector('#start').onclick = () => send('start', { muted: true, seed: 42 });
document.querySelector('#dispose').onclick = () => {
if (port) { disposeSeq = seq + 1; send('dispose'); }
};
frame.src = './g/template/';Every command needs a monotonically increasing seq; acknowledgements include that sequence, state, mute setting and tier. Wait for the matching disposal acknowledgement before removing the frame. See transport for retry timing, fallback and port ownership.
Start, pause and input ownership
Send start after readiness and on retry. If a touch starts play, include its normalized game coordinates in start.input. A held touch also needs a stable pointer id, held: true and continued forwarding through window.__ppKit.pointer. A vertical feed gesture must not become a game gesture.
Call __ppKit.gesture() synchronously from the host's user gesture handlers so the browser can unlock audio. __ppKit.busy(true) asks cooperative loaders to wait while the feed is moving; clear it when interaction ends. These same-origin helpers are optional in native containers and are specified in the transport contract.
Send pause before a game leaves the foreground. Stop its audible output as well as its simulation. Send resume when it becomes active again, and layout whenever safe-area insets change. The game may independently pause for blur or hidden visibility; respect its reported state.
Audio in a feed
A feed can provide window.__ppAudio before mounting games. It owns one session AudioContext and a channel per game. Only the active, unpaused game's channel should be audible. Games use the SDK's audio facade and must not close the shared context. See the complete audio ownership contract. Game-side effects, music, output buses and engine adapters are described in Audio.
Native and standalone containers
A top-level native web view uses window.__pp.send(name, payload) and portal:command after the host is detected; portal:host can announce it. The message names and lifecycle requirements remain the same. A browser tab without a parent or native host gets a synthesized init and starts on the first tap.
Verify the host
Test commands before readiness, held-touch handover, hidden/blur holds, restart and disposal. Verify sequence acknowledgements, silence while paused, and no frames or messages after disposal acknowledgement. Start with the runnable host fixture. Use conformance fixtures and browser checks together; reducer vectors alone do not test a host. Same-origin frames are part of the host's trust boundary, not isolation for untrusted game scripts.