SDK Internals
This page documents the internal architecture of the FanFest SDK for advanced developers who need to understand how the SDK communicates with the embedded frontend, manages state, and handles authentication behind the scenes.
SDK Class Architecture
The SDK is structured as a composition of specialized services, each responsible for a single domain concern. All services receive shared state and a logger through constructor injection.
Initialization Sequence
When FanFestSDK.init() is called, the following happens in order:
- Parse and validate options via Zod schema (
SDKOptionsSchema) - Initialize state --
SDKState.init(options)stores parsed options - Initialize toast notification manager -- sets up the notification container in the DOM
- Initialize embed service -- starts watching for
#fanfest-embedcontainers via MutationObserver - Initialize auth service -- sets up the
AuthIframeBrokerto listen for PostMessage events - Initialize tracker -- finds all
[data-fanfest-track]elements and attaches event listeners; starts a MutationObserver for dynamically added elements - Fetch web app info -- calls
GET /public/web-appsto retrieve channel ID and configuration
PostMessage Protocol
The SDK uses two separate PostMessage channels, each with its own broker, message namespace, and Zod validation schema.
Embed Broker (fanfest:embed-message)
Handles communication between the SDK and the FanFest frontend iframe (the visible embed). All messages are wrapped in an envelope:
{
type: "fanfest:embed-message",
data: { type: MessageType, ...payload }
}Message Types
| Type | Direction | Purpose | Payload |
|---|---|---|---|
embed-ready | Embed -> SDK | Iframe has loaded and is ready to receive auth state | (none) |
authentication | SDK -> Embed | Sends current authentication state to the embed | jwt, validated_identifiers, matches_identity, auth_channel |
authentication-finished | Embed -> SDK | Embed completed an in-iframe authentication flow | user: { id, email, username } |
Embed Communication Flow
Origin Validation
Both brokers validate event.origin against SDKOptions.appOrigin (defaults to https://fanfest.vip). Messages from any other origin are silently ignored. All message payloads are validated through Zod schemas -- malformed messages are logged as errors and discarded.
Auth Iframe Broker (fanfest:async-flow-message)
Handles communication with the hidden authentication iframe used during silent SSO flows. This iframe is invisible (1x1px) and is created/destroyed per authentication attempt.
{
type: "fanfest:async-flow-message",
data: { type: MessageType, ...payload }
}Message Types
| Type | Direction | Purpose | Payload |
|---|---|---|---|
validate-authentication:success | Auth Iframe -> SDK | Silent auth completed -- code and state available | code, state, redirect_uri |
validate-authentication:error | Auth Iframe -> SDK | Silent auth failed | error (string) |
When the SDK receives a validate-authentication:success message, it:
- Calls
POST /auth/silent-validationwith the received code, state, and redirect URI - Parses the response to extract user info and auth token
- Stores the auth state in
SDKAuthState(backed by localStorage) - Sends an
authenticationmessage to all active embed iframes - Destroys the hidden auth iframe
State Management
The SDK uses a custom Observable pattern for reactive state management, avoiding any framework dependency.
Observable Architecture
Key Design Decisions
SimpleObservableSubjectholds the last emitted value (lastState). New subscribers receive this value immediately on subscribe (configurable vianotifyOnSubscribe).SimpleObservableValueextendsSubjectwith a synchronousvaluegetter/setter, used for simple reactive values likeisAuthenticating.ObservableDumbStorageValuewrapslocalStoragewith a custom serializer (Zod-validated read, JSON write). This powersSDKAuthState, persisting auth tokens across page reloads.- Notification is encapsulated via a
Symbol-keyed method. Only classes extendingSimpleObservablecan trigger notifications, preventing external code from emitting events.
State Tree
SDKState
+-- options: SDKOptions (immutable after init)
| clientId, apiBase, appOrigin, iframe settings, callbacks
+-- authState: SDKAuthState
| +-- value: ObservableDumbStorageValue<SDKAuthObject | null>
| | authToken, user, validatedIdentifiers, authChannelType
| +-- isAuthenticating: SimpleObservableValue<boolean>
+-- channelInfo: SDKChannelInfoState
| +-- value: ObservableDumbStorageValue<ChannelInfo | null>
| channel_id, channel settings
+-- actionRegistry: SDKActionRegistry
Maps action aliases to channel action IDsEvent Tracking System
The SDKTracker provides both declarative (HTML data-attribute) and imperative (JavaScript API) tracking.
Declarative Tracking
The tracker uses a MutationObserver to watch the entire document for elements with data-fanfest-track attributes. It handles:
- Initial scan: On
init(), finds all existing[data-fanfest-track]elements - Dynamic elements: Observes
childList,subtree, and attribute changes to track elements added or removed after initialization - Cleanup: When tracked elements are removed from the DOM, their event listeners are automatically cleaned up
Data Attributes
| Attribute | Purpose | Required |
|---|---|---|
data-fanfest-track | Action ID or alias | Yes |
data-fanfest-on | Event trigger + modifiers (e.g., click:preventDefault) | No (defaults to click) |
data-fanfest-description | Human-readable action description | No |
data-fanfest-experience-id | Groups related actions into an experience | No |
data-fanfest-object-id | External object identifier for deduplication | No |
data-fanfest-meta-* | Custom metadata key-value pairs | No |
Supported Events and Modifiers
Only three DOM events are supported: click, render, submit.
renderevents fire immediately when the element is first observed (no DOM event listener attached)clickandsubmitattach standard event listeners with configurable modifiers
Modifiers are appended with colons: data-fanfest-on="click:stop:preventDefault".
| Modifier | Effect |
|---|---|
stop | Calls event.stopPropagation() |
preventDefault | Calls event.preventDefault() |
capture | Registers listener in capture phase |
once | Removes listener after first invocation |
Event Delivery
Tracked events flow through a pipeline:
DOM Event -> SDKTracker -> onTrackEvent observable -> SDK.trackEvent() -> LoyaltyService -> REST APIThe LoyaltyService sends events to POST /public/external-actions/ingest with:
channelActionId: Resolved from the action registry (alias -> ID) or used as-isx-app-idheader: The SDK'sclientIdauthorizationheader: Current auth token (if authenticated)- Metadata including SDK version stamp
Iframe Sandboxing and Security Model
Embed Iframe
The FanFest embed runs in a sandboxed iframe with a minimal permission set:
sandbox="allow-same-origin allow-scripts allow-forms allow-modals allow-popups"Feature policy permissions:
allow="autoplay; camera; fullscreen; microphone; clipboard-read; clipboard-write; self {host-protocol}://{host-hostname}"The embed URL is constructed as {appOrigin}/{channelId}?embed=true, where appOrigin defaults to https://fanfest.vip.
Hidden Auth Iframe
The silent authentication iframe is:
- Invisible:
visibility: hidden,opacity: 0, 1x1px,position: fixedat top-left - Temporary: Created when
login()is called, destroyed after auth completes (success or error) - No sandbox: The auth iframe does not use the
sandboxattribute because it needs to navigate to external SSO providers and redirect back
Security Boundaries
| Boundary | Mechanism |
|---|---|
| SDK -> Embed communication | PostMessage with origin check against appOrigin |
| SDK -> Auth iframe communication | PostMessage with origin check against appOrigin |
| Embed iframe permissions | sandbox attribute restricts capabilities |
| Auth token storage | localStorage with Zod validation on read |
| API authentication | authorization header + x-app-id header |
Build Outputs and CDN Deployment
The SDK is built with Vite in library mode, producing three output formats from a single source:
| Format | Output Path | Use Case |
|---|---|---|
| IIFE | {version}/fanfest-sdk.js | Script tag inclusion -- exposes window.FanFestSDK |
| ESM | {version}/esm/fanfest-sdk.esm.js | ES module import for bundled applications |
| CJS | {version}/cjs/fanfest-sdk.cjs.js | CommonJS require for Node.js/legacy bundlers |
Global Entry Point
The IIFE build attaches the SDK to window.FanFestSDK. The entry point (src/index.ts) ensures only one SDK instance exists:
function main(): SDK {
if (window.FanFestSDK) return window.FanFestSDK;
const sdk = new SDK();
window.FanFestSDK = sdk;
return sdk;
}Public API Surface
The SDK exports five public functions, all bound to the singleton instance:
| Export | Type | Description |
|---|---|---|
init(options) | (options: unknown) => Promise<void> | Initialize the SDK with configuration |
trackEvent(payload) | (payload: TrackEventPayload) => Promise<TrackEventResponse> | Track a loyalty event |
login() | () => Promise<void> | Initiate silent SSO authentication |
logout() | () => Promise<void> | Clear auth state |
registerActionMapping(payload) | (payload: SDKActionRegistryPayload) => void | Map action aliases to channel action IDs |
Build Configuration
- Target: ES2019 (broad browser support)
- Minification: Production only
- Source maps: Always enabled
- Version stamping:
window.__SDK_VERSION__is defined at build time frompackage.json - No external dependencies: All dependencies are bundled into the output (no external chunks)
CDN URL Pattern
SDK builds are deployed to S3 and served via CloudFront:
https://sdk.production.fanfest.vip/{version}/fanfest-sdk.js # IIFE
https://sdk.production.fanfest.vip/{version}/esm/fanfest-sdk.esm.js # ESM
https://sdk.production.fanfest.vip/latest/fanfest-sdk.js # Latest IIFENext Steps
- System Architecture -- High-level platform overview and EventBridge flows
- Quickstart -- Get started with step-by-step instructions
- SDK Methods Reference -- Complete method documentation
- Tracking Examples -- Implementation patterns
