Iframe Embedding
The Predictu iframe app is the primary interface for casino players. It renders the complete prediction market trading experience - market discovery, event pages, trading tickets, portfolio management - all within a sandboxed <iframe> embedded in the operator's casino site. This guide covers every aspect of the embedding process.
Embed URL Format
The iframe source URL includes query parameters that activate embed mode and identify the operator:
https://app.predictu.com?embed=true&operator=<slug>| Parameter | Required | Type | Description |
|---|---|---|---|
embed | Yes | boolean | Must be true. Activates embed mode, which hides the standalone navigation chrome (top bar, sidebar, footer), enables the PostMessage Bridge, and activates the Branding Injector. |
operator | Yes | string | The operator's URL-safe slug (e.g. example-casino). Used to look up operator configuration, branding, and allowed origins from Supabase. |
?embed=true, the app renders in standalone mode with its own navigation, login page, and default Predictu branding. This is useful for development and debugging.Full Embed Code
The recommended embed snippet with all necessary attributes:
<!-- Predictu Casino - Prediction Markets Embed -->
<div id="predictu-container" style="position: relative; width: 100%; height: 100vh;">
<!-- Loading skeleton (shown until iframe sends predictu:ready) -->
<div
id="predictu-loader"
style="
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
background: #0a0f0a; color: #5a6a5a; font-family: system-ui;
"
>
Loading prediction markets...
</div>
<iframe
id="predictu-iframe"
src="https://app.predictu.com?embed=true&operator=example-casino"
style="
position: relative; z-index: 1;
width: 100%; height: 100%; border: none;
"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
allow="clipboard-write"
loading="lazy"
title="Predictu Prediction Markets"
></iframe>
</div>
<script>
const PREDICTU_ORIGIN = "https://app.predictu.com";
const iframe = document.getElementById("predictu-iframe");
const loader = document.getElementById("predictu-loader");
// Initialize once the iframe document loads
iframe.addEventListener("load", () => {
iframe.contentWindow.postMessage({
type: "predictu:init",
user: {
id: "player_12345",
displayName: "John Doe",
email: "john@example.com",
},
balance: 500.00,
operatorId: "YOUR_OPERATOR_UUID",
}, PREDICTU_ORIGIN);
});
// Listen for events from the iframe
window.addEventListener("message", (event) => {
if (event.origin !== PREDICTU_ORIGIN) return;
if (!event.data?.type?.startsWith("predictu:")) return;
switch (event.data.type) {
case "predictu:ready":
// Hide the loading skeleton
if (loader) loader.style.display = "none";
break;
case "predictu:balance":
// Sync balance to your UI
console.log("Balance:", event.data.balance);
break;
case "predictu:trade":
// Log or process trade events
console.log("Trade:", event.data);
break;
case "predictu:navigate":
// Optional: sync URL for deep linking
console.log("Navigate:", event.data.path);
break;
}
});
</script>Sandbox Attributes
The sandbox attribute restricts what the iframe can do. Predictu requires the following sandbox tokens:
| Token | Required | Purpose |
|---|---|---|
allow-scripts | Yes | Required for the React application to run. Without this, the iframe renders a blank page. |
allow-same-origin | Yes | Required for Supabase authentication cookies and local storage. Without this, auth tokens cannot be persisted and every page load requires re-authentication. |
allow-popups | Recommended | Allows confirmation dialogs and external link navigation (e.g. market source links). Can be omitted if you handle all navigation via PostMessage. |
allow-forms | Recommended | Required for the login/register forms in standalone mode. In pure embed mode (PostMessage init), this can be omitted, but it is safer to include it. |
allow-top-navigation. This would let the iframe redirect the entire parent page, which is a security risk. Predictu does not require it.Feature Policies (allow attribute)
The allow attribute controls which browser features the iframe can access:
| Policy | Required | Purpose |
|---|---|---|
clipboard-write | Recommended | Allows the trading UI to copy share links, referral codes, and market URLs to the clipboard. Without this, "Copy Link" buttons will silently fail. |
Features the iframe does not require: camera, microphone, geolocation, payment, fullscreen. Do not add these unless you have a specific reason.
Responsive Sizing
The iframe app is fully responsive and adapts to its container dimensions. Here are recommended sizing strategies:
Full Viewport (Recommended)
The simplest approach - the iframe fills the entire content area of your page:
<iframe
src="..."
style="width: 100%; height: 100vh; border: none;"
></iframe>Fixed Height Panel
If the prediction markets are one section of a larger page:
<iframe
src="..."
style="width: 100%; height: 800px; border: none; border-radius: 12px;"
></iframe>Dynamic Height (Auto-resize)
Listen for the predictu:navigate message and adjust the iframe height based on the route. Market list pages need more height than individual market pages:
const HEIGHT_MAP = {
"/discover": "100vh",
"/watchlist": "80vh",
"/portfolio": "70vh",
};
window.addEventListener("message", (event) => {
if (event.origin !== PREDICTU_ORIGIN) return;
if (event.data?.type === "predictu:navigate") {
const height = HEIGHT_MAP[event.data.path] || "100vh";
document.getElementById("predictu-iframe").style.height = height;
}
});Mobile Considerations
On mobile devices, the iframe app renders a mobile-optimized layout with a bottom navigation bar. For the best mobile experience:
- Set
width: 100%and avoid fixed pixel widths below 360px. - Use
height: 100dvh(dynamic viewport height) instead of100vhto account for mobile browser chrome. - Avoid wrapping the iframe in a scrollable container - the iframe manages its own scroll internally.
- Ensure your parent page does not set
overflow: hiddenon the body when the iframe has focus, as this can interfere with touch scrolling.
/* Mobile-friendly iframe container */
.predictu-container {
width: 100%;
height: 100dvh;
height: 100vh; /* fallback for older browsers */
position: relative;
overflow: hidden;
-webkit-overflow-scrolling: touch;
}Loading States
The iframe takes 1–3 seconds to load depending on network conditions. Implement a loading state to prevent a blank flash:
Skeleton Pattern
Show a loading skeleton behind the iframe, then hide it when you receive predictu:ready:
<!-- Position skeleton behind iframe -->
<div style="position: relative;">
<div id="skeleton" style="
position: absolute; inset: 0; z-index: 0;
background: #0a0f0a;
display: flex; align-items: center; justify-content: center;
font-family: system-ui; color: #5a6a5a;
">
<div>
<div style="
width: 40px; height: 40px; border: 3px solid #1a2a1a;
border-top-color: #22c55e; border-radius: 50%;
animation: spin 0.8s linear infinite;
"></div>
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
</div>
</div>
<iframe id="predictu-iframe" ... style="position: relative; z-index: 1;" />
</div>
<script>
window.addEventListener("message", (event) => {
if (event.data?.type === "predictu:ready") {
document.getElementById("skeleton").style.display = "none";
}
});
</script>Timeout Fallback
Add a timeout to handle cases where the iframe fails to load or send the ready signal:
let ready = false;
window.addEventListener("message", (event) => {
if (event.data?.type === "predictu:ready") {
ready = true;
document.getElementById("skeleton").style.display = "none";
}
});
// Fallback: hide skeleton after 10 seconds even if ready wasn't received
setTimeout(() => {
if (!ready) {
document.getElementById("skeleton").innerHTML =
'<p style="color: #ef4444;">Failed to load. Please refresh.</p>';
}
}, 10000);Origin Whitelist Configuration
The PostMessage bridge validates the event.origin of every incoming message against the operator's registered origins. This is a critical security mechanism that prevents unauthorized sites from sending commands to the iframe.
How Origins Work
An origin is the combination of protocol + hostname + port:
| URL | Origin |
|---|---|
https://www.example-casino.com/casino/markets | https://www.example-casino.com |
https://example-casino.com | https://example-casino.com |
http://localhost:3002/test | http://localhost:3002 |
https://staging.example-casino.com:8443 | https://staging.example-casino.com:8443 |
https://example-casino.com and https://www.example-casino.com are different origins. Add both if your site is accessible at either.Managing Origins
Origins are managed through the Operator Dashboard or the iframe-origins API:
/api/iframe-originsReturns the list of allowed origins for your operator.
/api/iframe-originsAdd a new origin. Body: { "origin": "https://www.example-casino.com" }
/api/iframe-origins?origin=https://www.example-casino.comRemove an origin from the whitelist.
Development Mode
In development (NODE_ENV=development), the PostMessage bridge allows all localhost origins regardless of the whitelist. This makes local development easier, but never rely on this in production. The allowed check logic:
function isOriginAllowed(origin: string): boolean {
// Exact match against whitelist
if (allowedParentOrigin && origin === allowedParentOrigin) return true;
// Dev convenience: allow localhost
if (isDevelopment() && isLocalhostOrigin(origin)) return true;
return false;
}Embed Mode Behavior Changes
When ?embed=true is set, the iframe app makes several behavioral adjustments:
| Feature | Standalone Mode | Embed Mode |
|---|---|---|
| Navigation chrome | Full top bar, sidebar, footer | Hidden - only the content area renders |
| Authentication | Login/register forms via Supabase Auth | PostMessage predictu:init from parent |
| Balance source | Operator’s wallet via S2S callbacks | Initial balance from predictu:init, updates via Wallet Adapter |
| Branding | Default Predictu theme | Operator's custom branding via BrandingInjector |
| External links | Navigate directly | Open in new tab (respects allow-popups sandbox) |
| PostMessage Bridge | Inactive | Active - sends ready, balance, trade, navigate events |
Cross-Origin Considerations
Content Security Policy
If your casino site uses a strict Content Security Policy, you need to allow the Predictu origin in the frame-src directive:
Content-Security-Policy: frame-src https://app.predictu.com;X-Frame-Options
The Predictu iframe app sets X-Frame-Options: ALLOWALL to permit embedding. Your parent page should not set X-Frame-Options: DENY orSAMEORIGIN on the page that contains the iframe - those headers apply to the page itself being framed, not to iframes within it.
Third-Party Cookies
Some browsers (Safari, Brave) restrict third-party cookies by default. Since the iframe is on a different origin than the parent page, Supabase auth cookies may not persist. The PostMessage-based predictu:init authentication flow is designed to work without cookies - user identity comes from the parent, not from stored credentials.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Blank iframe | Missing allow-scripts in sandbox | Add allow-scripts to the sandbox attribute |
| Iframe shows login page | ?embed=true missing or predictu:init not sent | Check URL params and verify PostMessage init fires after iframe load |
| Default Predictu branding | Operator slug incorrect or branding not configured | Verify the operator param matches your slug in the Operator Dashboard |
| PostMessage events not received | Origin not in whitelist | Add your exact origin (including protocol and port) to the Operator Dashboard |
| Auth cookies lost on refresh | Third-party cookie blocking | This is expected in embed mode. Re-send predictu:init on each page load. |
| "Copy Link" button does nothing | Missing clipboard-write in allow attribute | Add allow="clipboard-write" to the iframe element |
| iframe content not scrollable on iOS | Parent container has overflow: hidden | Remove overflow restriction or add -webkit-overflow-scrolling: touch |
