Gamebus Docs

Custom pages

Add a custom page to the GameBus app.

Use this for full-page tools

A custom page is an external page rendered inside the GameBus app through an 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

DirectionTypeMeaning
Child to parentIFRAME_READYThe child has mounted and is ready to receive data.
Parent to childMENU_ITEMThe resolved menu item that opened this page, including the AppEmbedPage configuration.
Parent to childINPUT_COLLECTIONSData fetched from the AppEmbedPage input collections.
Child to parentACTIVITYStore an activity and show normal success feedback.
Child to parentSILENT_ACTIVITYStore an activity without normal user-facing success feedback.
Child to parentNAVIGATEAsk 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.org

Use 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/dashboard

Use 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: 10

Use 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.

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=progress

These 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_ITEM and INPUT_COLLECTIONS to that same origin.
  • The child should also verify event.origin when it knows the GameBus origin.
  • Activity messages must use an activity template allowed for the menu item and iframe origin.
  • NAVIGATE should 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

ProblemCheck
Page loads but receives no dataMake sure the child sends IFRAME_READY after installing its message listener.
Activity is rejectedCheck the menu item's allowed embedded activity provider origin and activity template reference.
Navigation failsSend an internal path such as /activities, not an external URL.
The header should not showConfigure the page as EMBED_FULLSCREEN.
The page appears without contextCheck 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.

On this page