Contract & SDK
This document describes the canonical contract between ResultFly and an external game delivered through the ExternalGame component. It also outlines the lightweight SDK surface we expose to make implementation easier for partners.
Lifecycle overview
- Navigation — user reaches a page node that contains the external block. The runtime constructs the iframe URL with context query parameters.
- Iframe boot — once the iframe
loadevent fires, ResultFly sends apostMessagewithtype: "resultfly:init"to the iframe’scontentWindow. - Ready acknowledgement — the game inspects the payload and answers with
type: "external-game.ready". Until this happens the wrapper shows a loading state. - Gameplay — the parties exchange messages defined below (state updates, score reports, resizing, pause/resume).
- Completion — the game announces completion or an unrecoverable error. The wrapper transitions the UI and records analytics; the hosting flow may continue to the next node based on studio logic.
If the iframe never acknowledges the init message, the wrapper times out after the configured timeoutMs and surfaces an error block to the user.
Initialization payload
resultfly:init has the following structure:
type InitPayload = {
session: {
id: string; // Unique ResultFly session id
nodeId: string; // Current page node id
componentId: string; // ExternalGame instance id
locale: string; // e.g. "en-US"
preview: boolean; // True in studio preview
};
user: {
participantId: string; // Stable identifier for analytics
traits?: Record<string, string | number | boolean | null>;
};
experiment?: {
campaignId: string;
groupId?: string;
};
config: unknown; // Editors' initPayload blob
theme: {
palette: Record<string, string>;
typographyScale: string;
};
};Fields mirror data already available to native controls. Most properties are read-only and meant for analytics correlation rather than tampering.
Messages sent by ResultFly
| Type | When | Payload |
|---|---|---|
resultfly:init | Immediately after iframe load | InitPayload |
resultfly:pause | When the host page is hidden or paused by Studio | { reason: "visibilitychange" | "manual" } |
resultfly:resume | When the page becomes active again | {} |
resultfly:update-config | When editors tweak initPayload in preview without reload | { config: unknown } |
Games must ignore unknown message types to stay forward-compatible.
Messages sent by the game
All outbound messages must use postMessage with targetOrigin matching the parent origin the iframe booted from. The wrapper rejects messages whose origin is not in the allowedOrigins list configured on the component.
| Type | Required | Purpose | Payload |
|---|---|---|---|
external-game.ready | ✅ | Signals that assets are loaded and the UI can be shown. | { capabilities?: string[] } |
external-game.progress | ➖ | Inform ResultFly about intermediate milestones (levels, lives, etc.). | { step: string; value?: number; meta?: Record<string, unknown> } |
external-game.score | ➖ | Provide a numeric score for leaderboards or rewards. | { value: number; unit?: string; meta?: Record<string, unknown> } |
external-game.complete | ✅ | Gameplay finished successfully. | { outcome: "win" | "loss" | "neutral"; durationMs?: number; rewards?: Record<string, unknown> } |
external-game.error | ✅ on failure | Report unrecoverable issues. Triggers fallback UI. | { code: string; message?: string; detail?: unknown } |
external-game.resize | ➖ | Request a height change once layout is known. | { height: number } |
external-game.log | ➖ | Optional debugging hook visible only in Studio preview. | { level: "info" | "warn" | "error"; message: string; payload?: unknown } |
The wrapper automatically enriches each inbound message with session metadata before forwarding it to analytics/event buses.
SDK surface
To avoid hand-writing postMessage plumbing, we expose a minimal SDK that games can import. The SDK provides:
import { createResultFlyGame } from "@resultfly/external-game-sdk";
const game = createResultFlyGame();
game.onInit(({ session, config }) => {
// start rendering once data is ready
});
game.reportScore({ value: 42 });
game.complete({ outcome: "win" });createResultFlyGame()registers message listeners and verifies the parent origin automatically.onInit,onPause, andonResumehelpers wrap the inbound messages listed above.reportProgress,reportScore,complete,fail,resize, andloghelpers format outbound messages and guard against rate limits (e.g., no more than 1 resize per 250 ms).- During development the SDK can run in “strict” mode to throw descriptive errors when the contract is violated.
The SDK itself is optional; custom frameworks can speak the protocol directly as long as messages follow the schema.
Security and sandboxing
- ResultFly enforces
sandboxattributes to keep the iframe isolated from the host document.allow-same-originis disabled unless the target origin is explicitly trusted by the organization. - Only HTTPS URLs are accepted. Mixed-content loads are blocked at publish time.
- The wrapper validates the
originof every inboundpostMessageand silently ignores unknown or malformed payloads. - If the iframe navigates to a different origin at runtime, communication stops until the new origin matches the allowlist. This prevents phishing or malicious redirects.
- The platform can inject a watchdog that tears down the iframe if it uses more than the allotted execution time or memory (values TBD), ensuring one runaway game does not compromise the overall experience.
Validation and publishing workflow
- Authoring — editors add the component, provide URLs, and optionally drop in the developer’s configuration JSON. Studio warns if required fields are missing.
- Preview handshake — when the page runs in preview, Studio verifies that
external-game.readyarrives within the timeout and that completion/error messages conform to the schema. Failures surface inline so editors can notify the developer. - Publishing checks — the build system persists the allowlist, timeout, and schema version alongside the experience. It also stores a checksum of the SDK contract (for auditability) in
platform-spec. - Runtime telemetry — analytics events include a
componentType: "external_game"tag and thecomponentId, so we can monitor MTTR/SLA per partner.
Only after the block passes preview validation is the “Publish” button enabled, ensuring that external code is contract-compliant before it reaches end users.