Extension Runtime
runtime.* API for in-storefront UI — sandboxed, permission-filtered, real.
Built (Phases 1–4), never run against a real deployed extension
This is genuinely implemented — not a design doc. The Runtime SDK
(sandbox-runtime repo) and Host SDK (store-front repo) both exist,
are tsc/build-clean, and unit-tested. What hasn't happened yet: a real
extension bundle, running against a real deployed sandbox.prebit.in,
with a real signed release, on real production traffic. Treat this page
as accurate to the code, not as "shipped and proven."
What this is
An installed app can already do backend things (Partner API + webhooks). The Extension Runtime is what lets an app render real UI on a merchant's storefront — a chat widget, a promo banner, a reviews block — without ever giving your code DOM, cookie, or storage access to the page it's on.
Storefront (store-front) sandbox.prebit.in (sandbox-runtime)
┌─────────────────┐ ┌───────────────────────────┐
│ Host SDK │ postMessage │ Runtime SDK │
│ iframe lifecycle,│ (versioned, │ owns the whole wire │
│ origin checks │ origin-checked) │ protocol — you only call │
└─────────────────┘ ───────────────────► │ runtime.* │
│ │ │
│ Your reviewed code │
│ (content-hash-pinned CDN) │
└───────────────────────────┘You never touch postMessage, pick a protocol version, or validate an
origin — the Runtime SDK does that once, correctly, for every extension.
The Runtime interface
interface Runtime {
context: Partial<Record<"store" | "theme" | "customer" | "product" | "collection" | "currency" | "locale" | "cart", unknown>>;
onContextChange(cb: (context: Runtime["context"]) => void): void;
cart: { add(payload: unknown): Promise<unknown> };
navigation: { open(payload: unknown): Promise<unknown> };
modal: { open(payload: unknown): Promise<unknown> };
toast: { show(payload: unknown): Promise<unknown> };
publish(payload: { type: string; productId?: string; data: unknown }): Promise<{ ok: boolean; contentId?: string; error?: string }>;
}Your extension's default export:
export default {
initialize(runtime) { /* runs once, before mount */ },
mount(root: HTMLElement) { /* render into root */ },
update(context) { /* context changed */ },
visibilityChange(state: DocumentVisibilityState) { /* tab hidden/visible */ },
destroy() { /* cleanup */ },
};routeChange/themeChange are declared in the lifecycle contract but
have no real trigger in store-front's current architecture — every
storefront page is a full server-rendered document with no client
router, so a real navigation just tears down and recreates the whole
iframe instead. Left honestly unimplemented, not faked.
Context API
runtime.context is read-only, and permission-filtered: a field is
only ever populated if your install's granted permissions cover it.
| Field | Requires permission |
|---|---|
product, collection | product.read |
cart | cart.read |
customer | customer.events |
An app without customer.events never receives a populated
context.customer — not even a redacted one.
Actions API
The only way an extension affects the storefront — no DOM access exists, so there is nothing else to sanitize:
runtime.cart.add(payload)runtime.navigation.open({ url })runtime.modal.open(payload)— no storefront-side implementation yet, acks withok:falseruntime.toast.show(payload)— same,ok:falsetoday
Permissions
Extends the same scopes/capabilities system every other page describes,
with runtime-context permissions: product.read, cart.read,
customer.events, checkout.open, storage.write, content.publish.
Shown in the same install consent dialog as OAuth scopes — not a second
dialog.
runtime.publish() — content contribution
The one Actions-API-adjacent call that does not go through the Host SDK/postMessage bridge — it POSTs structured JSON directly to new-frontend, because content a merchant wants search-indexed (reviews, FAQs) is invisible to crawlers inside a sandboxed cross-origin iframe by design.
await runtime.publish({ type: "review", productId, data: { rating: 5, authorName: "...", body: "..." } });Structured data only, validated against a schema per type server-side
— never HTML. There is no code path where your bytes become markup.
Gated by the content.publish permission. Today, only type: "review" is
a real, schema-validated type — not a generic multi-type registry yet.
Version negotiation
Your manifest declares requires_runtime = ">=1.0". The handshake checks
this against the deployed Runtime SDK's own version and fails closed —
a merchant sees nothing (not a broken embed) rather than incompatible code
loading; you find out via App Health, not a customer-facing error.
Crash isolation
The Host SDK heartbeat-watches your iframe; a missed heartbeat gets reported through App Health and attempts exactly one auto-restart (fresh iframe, same claim — only works if the crash happened while the ~60s claim TTL is still valid).
What's genuinely not done
- Never run end-to-end against a real extension bundle or real production traffic.
modal.open()/toast.show()have no real storefront-side primitive.- Only one
runtime.publish()content type (review) exists. - Background-communication performance work and stylesheet preloading are explicitly deferred — no measured problem to optimize against yet.