Runtime & the bridge
How phone apps actually run — the sandboxed iframe model, the postMessage bridge, the bootstrap snapshot, and lifecycle events.
The Phone SDK is the same object everywhere, but how it talks to the phone depends on who's running it. Understanding this makes the whole SDK click.
Two execution modes
When you call createHelixPhoneSdk({ appId }), the SDK picks one of two transports automatically:
Your app runs inside a sandboxed iframe in the phone shell. It has no access token. The SDK
routes every call through the bridge — a postMessage channel to the phone shell, which performs
the call on your behalf and enforces permissions. This is the mode third-party apps use.
// No token — the SDK detects it's framed and uses the bridge.
const phone = createHelixPhoneSdk({ appId: "studio.example" });A first-party context (or a server-side caller) passes an access token. The SDK calls the phone
REST API (/api/v1/phone/...) directly with that token.
const phone = createHelixPhoneSdk({ appId: "wallet", accessToken });The SDK decides with one rule: if it's running inside another window (window.parent !== window) and
has no access token, it uses the bridge. Otherwise it calls the API directly. Your app code is
identical in both modes — you never branch on it.
The bridge protocol
The bridge is a request/response protocol over postMessage, secured by a per-session
helixBridgeNonce (passed to the iframe as a query param). The shape:
// app → shell
window.parent.postMessage({
source: "helix-phone-app",
appId, bridgeNonce, requestId,
method: "notifications.push", // namespaced SDK method
params: { title, body },
}, "*");
// shell → app
{
source: "helix-phone",
bridgeNonce, requestId,
ok: true,
result: { /* … */ }, // or: ok:false, error: "scope_not_granted"
}Requests time out after 8 seconds. The nonce ensures one app can't impersonate another or read
another app's responses. You normally never write this by hand — the SDK does it — but it's why every
SDK method maps to a stable method string like media.pickFromAlbum or storage.set.
Why a bridge at all?
Third-party code never gets the player's token, never sees other apps' data, and can't call the platform directly. The shell is the only thing holding credentials; it brokers every call and checks the app's granted permissions first. That's what makes running untrusted apps safe.
Bootstrap — one snapshot of everything
On launch, an app reads a bootstrap snapshot that contains the account, installed apps, effective permissions, the launch context, and runtime config:
const runtime = await phone.runtime.bootstrap();
// runtime.account → the signed-in PhoneAccount
// runtime.apps → installed app manifests
// runtime.permissions → { [appId]: PhonePermissionScope[] }
// runtime.launchContext → how this launch happenedConvenience accessors wrap it:
const me = await phone.account.getCurrentUser(); // runtime.account
const ctx = await phone.runtime.launchContext(); // see below
const manifest = await phone.runtime.manifest(); // this app's manifestRuntime config
The bootstrap's runtime block tells you the environment you're in:
Prop
Type
Launch context
How did the app open? Deep link from a notification, the home screen, or a web URL?
type PhoneAppLaunchContext = {
appId: string;
url: string;
path: string;
source: "web" | "notification" | "shell";
receivedAt: string;
};Use it to route — e.g. a notification with a deepLink lands the user on the right screen.
Lifecycle events
The phone is foreground-only: only the active app runs. Subscribe to transitions so you can pause timers, save drafts, or refresh on resume:
const off = phone.lifecycle.on("suspend", () => saveDraft());
phone.lifecycle.on("resume", () => refresh());
phone.lifecycle.on("close", () => flush());
// later
off(); // unsubscribe| Event | Fires when |
|---|---|
foreground | The app becomes the active app. |
suspend | The app is backgrounded (another app opened, phone closed). |
resume | The app returns to the foreground. |
close | The app is being torn down. |
Persist on suspend, not on a timer
Because backgrounded apps are suspended, write important state to
phone.storage on suspend/close rather than relying on periodic
saves.