Custom pages
Add a custom page to the GameBus app.
Use this for full-page tools
AppEmbedPage menu item. It can receive menu context, use input collections, store activities, and ask GameBus to navigate.Custom pages are best for campaign dashboards, onboarding flows, partner tools, reporting views, and larger experiences that should feel like a page in the GameBus app.
Communication flow
Message protocol
| Direction | Type | Meaning |
|---|---|---|
| Child to parent | IFRAME_READY | The child has mounted and is ready to receive data. |
| Parent to child | MENU_ITEM | The resolved menu item that opened this page, including the AppEmbedPage configuration. |
| Parent to child | INPUT_COLLECTIONS | Data fetched from the AppEmbedPage input collections. |
| Child to parent | ACTIVITY | Store an activity and show normal success feedback. |
| Child to parent | SILENT_ACTIVITY | Store an activity without normal user-facing success feedback. |
| Child to parent | NAVIGATE | Ask the parent app to navigate to an internal GameBus path. |
Configure the AppEmbedPage
An embedded page is opened through a participant menu item associated with an AppEmbedPage.
title: Quiz dashboard
subtitle: Your current progress
route: /embed/[appEmbedPageId]
appEmbedPage:
type: EMBED_WITH_HEADER
url: https://pages.example.org/dashboard
inputCollections:
- key: participant
inputRequests:
- key: current
endpoint: /api/embed/participant/current
activityTemplates:
- reference: dashboard-action
embeddedActivityProviders:
- origin: https://pages.example.orgUse EMBED_WITH_HEADER when the GameBus header should remain visible. Use EMBED_FULLSCREEN when the iframe should use the full viewport and manage its own top-level layout.
Build the external page
The child page can be hosted outside GameBus.
Minimal Svelte example:
<script lang="ts">
type ParentMessage =
| { type: 'MENU_ITEM'; data?: { title?: string; subtitle?: string } }
| { type: 'INPUT_COLLECTIONS'; data?: Record<string, Record<string, unknown>> };
let menuItem: ParentMessage['data'] = $state();
let inputCollections: Record<string, Record<string, unknown>> = $state({});
let startedAt = new Date();
$effect(() => {
window.parent.postMessage({ type: 'IFRAME_READY' }, '*');
});
function handleMessage(event: MessageEvent<ParentMessage>) {
if (!event.data || typeof event.data !== 'object') return;
if (event.data.type === 'MENU_ITEM') {
menuItem = event.data.data;
}
if (event.data.type === 'INPUT_COLLECTIONS') {
inputCollections = event.data.data ?? {};
}
}
function storeDashboardAction() {
window.parent.postMessage(
{
type: 'ACTIVITY',
data: {
template: 'dashboard-action',
start: startedAt,
end: new Date(),
properties: [
{ template: 'action', obj: { value: 'opened-recommendation' } }
]
}
},
'*'
);
}
function openActivities() {
window.parent.postMessage({ type: 'NAVIGATE', data: '/activities' }, '*');
}
</script>
<svelte:window onmessage={handleMessage} />
<main>
<h1>{menuItem?.title ?? 'Dashboard'}</h1>
<p>{menuItem?.subtitle}</p>
<pre>{JSON.stringify(inputCollections, null, 2)}</pre>
<button type="button" onclick={storeDashboardAction}>Save action</button>
<button type="button" onclick={openActivities}>View activities</button>
</main>Receive menu item data
After IFRAME_READY, GameBus posts the resolved menu item.
type: MENU_ITEM
data:
id: app-menu-item-reference
title: Quiz dashboard
subtitle: Your current progress
appEmbedPage:
id: app-embed-page-reference
type: EMBED_WITH_HEADER
url: https://pages.example.org/dashboardUse MENU_ITEM for display labels, page context, or behavior that depends on the item configuration. Treat it as parent-provided data and validate fields before using them.
Receive input collections
The AppEmbedPage can define input collections. GameBus resolves them before or while the iframe loads, then sends them after IFRAME_READY.
type: INPUT_COLLECTIONS
data:
participant:
current:
name: Ada Lovelace
team: Analytical Engines
dashboard:
progress:
completedTasks: 7
totalTasks: 10Use input collections to prefill UI, show participant-specific context, calculate local state, or build activity properties.
Store activities from the page
Custom pages can post activities when the menu item is configured with an allowed embedded activity provider for the iframe origin.
window.parent.postMessage(
{
type: "ACTIVITY",
data: {
template: "dashboard-action",
start: new Date("2026-07-14T09:00:00.000Z"),
end: new Date("2026-07-14T09:05:00.000Z"),
properties: [
{
template: "action",
obj: { value: "recommendation-accepted" },
},
],
},
},
"*",
);ACTIVITY and SILENT_ACTIVITY are both accepted. For pages, neither closes the iframe because the page is already the main route. The difference is user-facing feedback: ACTIVITY shows success feedback, while SILENT_ACTIVITY stores quietly.
Navigate the parent app
Use NAVIGATE when the child should move the participant to another GameBus route.
window.parent.postMessage(
{
type: "NAVIGATE",
data: "/activities?filter=recent",
},
"*",
);The parent accepts internal same-origin paths only. These are valid:
/activities
/missions/current
/profile?tab=progressThese are rejected:
https://example.org
//example.org/path
javascript:alert(1)Security checklist
- The parent accepts messages only from
new URL(appEmbedPage.url).origin. - The parent sends
MENU_ITEMandINPUT_COLLECTIONSto that same origin. - The child should also verify
event.originwhen it knows the GameBus origin. - Activity messages must use an activity template allowed for the menu item and iframe origin.
NAVIGATEshould contain internal GameBus paths only.
function handleMessage(event: MessageEvent) {
if (event.origin !== "https://app.example.org") return;
if (!event.data || typeof event.data !== "object") return;
if (event.data.type !== "MENU_ITEM" && event.data.type !== "INPUT_COLLECTIONS") return;
// Use validated data here.
}Troubleshooting
| Problem | Check |
|---|---|
| Page loads but receives no data | Make sure the child sends IFRAME_READY after installing its message listener. |
| Activity is rejected | Check the menu item's allowed embedded activity provider origin and activity template reference. |
| Navigation fails | Send an internal path such as /activities, not an external URL. |
| The header should not show | Configure the page as EMBED_FULLSCREEN. |
| The page appears without context | Check the AppEmbedPage input collections and endpoint responses. |
Summary
To add a custom page, configure an AppEmbedPage menu item, load your external page URL, send IFRAME_READY, receive MENU_ITEM and INPUT_COLLECTIONS, then optionally post ACTIVITY, SILENT_ACTIVITY, or NAVIGATE messages back to GameBus.