Prebit Developer Docs

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.

FieldRequires permission
product, collectionproduct.read
cartcart.read
customercustomer.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 with ok:false
  • runtime.toast.show(payload) — same, ok:false today

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.

On this page