Appearance
Host bridge (iframe contract)
Surface Apps load in a cross-origin iframe inside Helix Platform Desktop. Some browser APIs are blocked in that context. Apps talk to the shell over postMessage using the host bridge contract (defined in @helix-platform/shared → host-bridge.ts).
When to use it
| Capability | Call from iframe? | Use bridge instead |
|---|---|---|
| Cognito JWT for Log Center / shell API | N/A | Session messages |
Notification.requestPermission() | No (blocked cross-origin) | Notification permission request |
| Web Push subscribe (VAPID) | Prefer Platform | Optional enablePush on the same request |
Platform stays a shell: it grants permission / owns the service worker; product logic stays in the Surface App.
Message types
| Direction | type | Purpose |
|---|---|---|
| Platform → app | helix-platform-session | { accessToken, apiBaseUrl } |
| App → Platform | helix-platform-session-request | Ask parent to (re)send session |
| App → Platform | helix-platform-notification-permission-request | { requestId, enablePush? } |
| Platform → app | helix-platform-notification-permission-result | { requestId, permission, pushEnabled?, error? } |
| App → Platform | helix-platform-toast | { title?, body, variant?, durationMs? } — ephemeral shell toast |
Shell toasts (app side)
Apps embedded in Platform should not show persistent inline status for shell-mediated actions (e.g. notification permission). Post a toast instead — Platform shows a short-lived popover near the taskbar.
ts
window.parent.postMessage(
{
type: 'helix-platform-toast',
title: 'Helix Patch',
body: 'Scan completed.',
variant: 'success', // info | success | warning | error
durationMs: 6000,
},
'*',
)Fire-and-forget — no reply. When requesting notification permission, Platform also shows toasts for granted / denied automatically.
Notification permission (app side)
ts
function isEmbedded(): boolean {
try {
return window.parent != null && window.parent !== window
} catch {
return true
}
}
export async function requestNotificationPermission(opts?: {
enablePush?: boolean
timeoutMs?: number
}): Promise<{ permission: NotificationPermission; pushEnabled: boolean }> {
if (!isEmbedded()) {
const permission = await Notification.requestPermission()
return { permission, pushEnabled: false }
}
const requestId = crypto.randomUUID()
return new Promise((resolve, reject) => {
const t = window.setTimeout(() => {
window.removeEventListener('message', onMessage)
reject(new Error('Platform did not respond to notification permission request'))
}, opts?.timeoutMs ?? 120_000)
function onMessage(event: MessageEvent) {
const data = event.data as {
type?: string
requestId?: string
permission?: NotificationPermission
pushEnabled?: boolean
error?: string
}
if (data?.type !== 'helix-platform-notification-permission-result') return
if (data.requestId !== requestId) return
window.clearTimeout(t)
window.removeEventListener('message', onMessage)
if (data.error && data.permission !== 'granted') {
reject(new Error(data.error))
return
}
resolve({
permission: data.permission ?? 'default',
pushEnabled: Boolean(data.pushEnabled),
})
}
window.addEventListener('message', onMessage)
window.parent.postMessage(
{
type: 'helix-platform-notification-permission-request',
requestId,
enablePush: opts?.enablePush !== false,
},
'*',
)
})
}Standalone (top-level) apps keep calling Notification.requestPermission() directly — the helper above detects embedding.
Platform host
HelixAppFrame listens for bridge requests, runs Notification.requestPermission() in the top-level frame, optionally registers Platform Web Push, and replies to event.source.
See also
- Shell API — HTTP notifications / push subscribe
- Shared types:
shared/src/host-bridge.ts