Advanced Embed Patterns
This page covers advanced embed integration patterns for developers who need more control over the FanFest embed behavior.
Custom Login Callback
The hostLoginFn callback lets you replace the embed's built-in login with your own authentication flow. When a user clicks "Login" inside the embed, the SDK calls your function instead of showing the FanFest login form.
FanFestSDK.init({
clientId: "your-web-app-id",
hostLoginFn: () => {
// Example: redirect to your Auth0 login
auth0Client.loginWithRedirect({
redirect_uri: window.location.origin + "/auth/callback",
});
},
});Requires SSO integration
Custom login callbacks only work when you have SSO configured. Without SSO, the embed uses FanFest's built-in authentication and the hostLoginFn is ignored.
How it works:
- The user clicks a login trigger inside the embed
- The embed sends a PostMessage to the SDK
- The SDK calls your
hostLoginFn - Your function handles the authentication (redirect, modal, etc.)
- After successful auth, the SDK's silent authentication syncs the user state back to the embed
Custom Rewards Callback
The hostRewardsFn callback lets you direct users to your own rewards experience instead of the embed's built-in rewards view.
FanFestSDK.init({
clientId: "your-web-app-id",
hostRewardsFn: () => {
// Example: open a rewards modal on your site
document.getElementById("rewards-modal").showModal();
},
});Requires SSO integration
Custom rewards callbacks only work when you have SSO configured. Without SSO, the embed handles rewards display internally and the hostRewardsFn is ignored.
Embed Lifecycle
The embed communicates with the host page through a PostMessage bridge. Here is the sequence of events during initialization:
Host Page SDK Embed Iframe
| | |
|-- init(options) ------->| |
| |-- Create iframe ------------>|
| |-- MutationObserver setup |
| | |
| |<-- EmbedReady ---------------|
| |-- Authentication ----------->|
| | |
| |<-- AuthenticationFinished ---|
| | |Key lifecycle events:
- SDK Initialization —
init()validates options, fetches web app config, and creates the embed iframe - Embed Ready — The iframe content loads and signals readiness via
embed-readyPostMessage - Authentication — The SDK sends the current auth state to the embed
- Authentication Finished — The embed confirms authentication is complete and reports the user state
The PostMessage bridge validates message origins against the configured appOrigin for security. Messages from unknown origins are silently ignored.
Host-Routed Links
When FanFest is embedded, share and referral URLs point back to the host page instead of fanfest.vip. The SDK preserves the host page's path, query string, and hash, and stores the FanFest route — relative to the embedded channel — in the ff_r query parameter. The SDK already knows the channel it's embedding, so the channel slug is omitted to keep the URL compact.
Example:
https://partner.example/fanfest?existing=1&ff_r=%2Fln%2Fabc#host-sectionWhen that URL loads, the SDK reads ff_r, re-prepends the configured channel, and boots the iframe directly into the matching FanFest route. If the route is invalid or external, the SDK ignores it and loads the configured channel root.
The legacy fanfest_route parameter is still read for backwards compatibility with links emitted by older SDK versions; values that point at another channel are rejected.
Multiple Embeds on One Page
The SDK supports multiple embed containers on the same page. Each <div id="fanfest-embed"> element gets its own iframe.
<!-- Main content area -->
<div id="fanfest-embed" style="width: 100%; height: 600px;"></div>
<!-- Sidebar widget -->
<div id="fanfest-embed" style="width: 300px; height: 400px;"></div>Same channel
All embed instances on a page share the same clientId and display the same channel. The SDK does not currently support showing different channels in different embed containers on the same page.
Behavior notes:
- All embed instances share the same authentication state — logging in through one embed authenticates all of them
- The SDK tracks each container independently and creates a separate iframe for each
- Removing a container from the DOM causes the SDK to automatically clean up that embed instance
- Dynamic containers are supported — add a
<div id="fanfest-embed">at any time and the SDK's MutationObserver picks it up
Auto-Resize Embed
The enableAutoHeightEmbed option makes the iframe height automatically follow the content inside the embed, so you do not need to set a fixed height on the container.
FanFestSDK.init({
clientId: "your-web-app-id",
enableAutoHeightEmbed: true,
});<!-- No fixed height — the iframe grows to fit content -->
<div id="fanfest-embed" style="width: 100%;"></div>How it works:
- After creating the embed iframe, the SDK lazy-loads the
@open-iframe-resizer/coreparent script - The parent script sends an
iframe-child-initpostMessage to the embed - The frontend detects embed mode and lazy-loads the child script, which attaches a
ResizeObservertodocument.documentElement - When the embed content height changes (navigation, expanded sections, etc.), the child sends an
iframe-resizedpostMessage with the new height - The parent script sets
iframe.style.heightto the reported value
Both scripts are lazy-loaded at runtime — this feature adds zero bytes to the SDK or frontend initial bundles.
Container requirements:
- Do not set a fixed
heighton the container — it will override the auto-resize - Use
min-heightif you want a floor (e.g., to prevent a collapsed state before the first resize message):
<div id="fanfest-embed" style="width: 100%; min-height: 300px;"></div>Cleanup: When the embed container is removed from the DOM, the SDK automatically calls unsubscribe() on the resize handle. No manual cleanup is required.
Browser compatibility: Chrome 64+, Safari 13.1+, Firefox 69+, Opera 51+.
Content Security Policy: If your host page uses a CSP, the auto-resize feature requires no additional directives — the library script is loaded from the same origin as the SDK bundle, and the postMessage exchange uses the existing allow-same-origin iframe sandbox permission.
Responsive Embed Sizing
The embed iframe fills 100% of its container's width and height. To make the embed responsive, style the container:
Fixed Height
<div id="fanfest-embed" style="width: 100%; height: 600px;"></div>Viewport-Relative Height
<div id="fanfest-embed" style="width: 100%; height: 80vh;"></div>Full-Page Embed
#fanfest-embed {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
}Responsive with Aspect Ratio
#fanfest-embed {
width: 100%;
aspect-ratio: 16 / 9;
}Mobile-Responsive Layout
#fanfest-embed {
width: 100%;
height: 600px;
}
@media (max-width: 768px) {
#fanfest-embed {
height: 100vh;
position: fixed;
top: 0;
left: 0;
}
}Action Mapping with registerActionMapping
The registerActionMapping method maps custom action names (used in your tracking code) to channel action IDs (configured in the FanFest dashboard). This is required for event tracking to correctly associate tracked events with leaderboard actions and point rewards.
Why Action Mapping Matters
When you track an event like FanFestSDK.trackEvent({ action: "purchase" }), the SDK needs to know which channel action ID this corresponds to. Action mappings create that link between your application's event names and FanFest's internal action system.
Three Input Formats
The SDK accepts three formats for registering action mappings. All three are validated at runtime and produce the same result internally.
Format 1: Single Object
Register one mapping at a time:
FanFestSDK.registerActionMapping({
action: "purchase",
channelActionId: "ca_abc123",
});Format 2: Array of Objects
Register multiple mappings in a single call:
FanFestSDK.registerActionMapping([
{ action: "purchase", channelActionId: "ca_abc123" },
{ action: "page_view", channelActionId: "ca_def456" },
{ action: "share", channelActionId: "ca_ghi789" },
]);Format 3: Bulk Record (Recommended)
Register multiple mappings using a key-value record where keys are action names and values are channel action IDs:
FanFestSDK.registerActionMapping({
purchase: "ca_abc123",
page_view: "ca_def456",
share: "ca_ghi789",
signup: "ca_jkl012",
referral: "ca_mno345",
});Recommended format
The bulk record format is the most concise and readable, especially when registering many mappings. We recommend this format for production use.
When to Register Mappings
Register action mappings after init() completes but before tracking any events:
await FanFestSDK.init({ clientId: "your-web-app-id" });
// Register all action mappings up front
FanFestSDK.registerActionMapping({
purchase: "ca_abc123",
page_view: "ca_def456",
share: "ca_ghi789",
});
// Now tracking works correctly
FanFestSDK.trackEvent({ action: "purchase" });Finding Channel Action IDs
Channel action IDs are created in the FanFest dashboard under your channel's leaderboard settings. Each action you create in the dashboard gets a unique ID that you use in your action mappings. See the Leaderboards guide for details on creating channel actions.
Validation Errors
If you pass an invalid payload to registerActionMapping, the SDK logs an error to the console but does not throw. This prevents a misconfigured mapping from breaking your application.
// This logs an error — values must be strings
FanFestSDK.registerActionMapping({
purchase: 12345, // Error: expected string
});Initialization Guard
The SDK prevents double initialization. If you call init() more than once, the second call is ignored and a warning is logged:
await FanFestSDK.init({ clientId: "your-web-app-id" });
await FanFestSDK.init({ clientId: "other-id" }); // Ignored, logs warningThis is safe to call in application code that might run multiple times (e.g., in a React useEffect or Vue onMounted hook).
Host-Relayed Navigator Events
SDK 1.9.0+
These features require SDK version 1.9.0 or later on the host page. Earlier versions don't run the navigator handler — the embed still works but share/copy/OAuth from inside the iframe behave as if there's no host. Update your <script src="..."> to point at 1.9.0/embed-sdk.js (or latest/embed-sdk.js).
When the FanFest experience runs inside your iframe, several browser APIs scope to the iframe's origin instead of yours:
| API | Without the relay | With the relay |
|---|---|---|
window.location | Returns the iframe URL (fanfest.vip/...) | Returns your host page URL |
navigator.share | "Share from fanfest.vip" sheet | "Share from your-domain.com" |
navigator.clipboard | Permission-denied on Safari | Writes from your origin |
window.location.assign | Navigates the iframe only | Navigates the host page |
| OAuth popup | Severed window.opener on Safari + iframe | Works (popup is host-sibling) |
The SDK transparently relays these calls through the host page so the embed feels like a native part of your site. Nothing on your side is required — just keep the SDK version current. Existing host integrations continue to work unchanged.
What this fixes
- Share / copy links point at
your-domain.com/path?fanfestEvent=show/...instead offanfest.vip/.... Users who paste the link land back on your site, not on FanFest's standalone domain. - Native share sheet shows your site's title and origin.
- Clipboard write works on Safari (Clipboard API permission is scoped to the top-level origin; iframe writes were rejected).
- Sign-in with Google works on Safari + iframe. Without the relay, Safari's COOP severs
window.openerand the auth-success postMessage from the OAuth popup never lands.
What you need to do
Nothing — the relay is automatic once SDK 1.9.0+ is loaded on your host page. The events are listed below for reference if you want to inspect or proxy them in your own code.
Protocol overview
The SDK installs a message listener on window and responds to a small set of ff-<event>:ask messages from the iframe:
| Event | Purpose | Iframe-side fallback if missing |
|---|---|---|
ff-location | Returns host window.location | Iframe window.location (legacy behaviour) |
ff-share | Calls navigator.share on the host | navigator.share from inside the iframe |
ff-clipboard | Calls navigator.clipboard.writeText on the host | navigator.clipboard from inside the iframe |
ff-navigate | Calls window.location.assign on the host | Iframe-only navigation |
ff-oauth | Opens the provider login popup from host context | None — Safari + iframe sign-in is broken without this |
All asks carry a requestId and a protocol version (v: 1); responses echo both. The SDK pins targetOrigin to the iframe's URL origin on replies and rejects asks from event.source windows that aren't tracked iframes.
If you embed multiple FanFest channels on the same page, the SDK matches each ask to the originating iframe and replies only to that one. There's no cross-talk between embeds.
Security Model
The embed uses several security mechanisms:
- Origin validation — PostMessage events are only accepted from the configured
appOrigin. Replies on the host-relayed navigator protocol are pinned to each iframe'ssrcorigin. - Iframe sandbox — The embed iframe runs with a restricted sandbox:
allow-same-origin,allow-scripts,allow-forms,allow-modals,allow-popups - Function binding —
hostLoginFnandhostRewardsFnare bound to thewindowobject to prevent exposing your function scope to the embed - Schema validation — All incoming PostMessage data is validated against a strict schema before processing
- Protocol versioning — Navigator events carry a
vfield; messages with an unknown version are dropped, so a partial deploy fails closed
Next Steps
- Configuration Reference — Full options reference with troubleshooting
- Event Tracking — Track user engagement events
- SSO Integration — Required for custom login/reward callbacks
- SDK Architecture — Internal SDK architecture and PostMessage protocol details
