HELIX 3 Docs
Helix Phone

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 happened

Convenience 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 manifest

Runtime 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
EventFires when
foregroundThe app becomes the active app.
suspendThe app is backgrounded (another app opened, phone closed).
resumeThe app returns to the foreground.
closeThe 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.

On this page