Skip to content

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:

  1. Parse and validate options via Zod schema (SDKOptionsSchema)
  2. Initialize state -- SDKState.init(options) stores parsed options
  3. Initialize toast notification manager -- sets up the notification container in the DOM
  4. Initialize embed service -- starts watching for #fanfest-embed containers via MutationObserver
  5. Initialize auth service -- sets up the AuthIframeBroker to listen for PostMessage events
  6. Initialize tracker -- finds all [data-fanfest-track] elements and attaches event listeners; starts a MutationObserver for dynamically added elements
  7. Fetch web app info -- calls GET /public/web-apps to 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:

typescript
{
  type: "fanfest:embed-message",
  data: { type: MessageType, ...payload }
}

Message Types ​

TypeDirectionPurposePayload
embed-readyEmbed -> SDKIframe has loaded and is ready to receive auth state(none)
authenticationSDK -> EmbedSends current authentication state to the embedjwt, validated_identifiers, matches_identity, auth_channel
authentication-finishedEmbed -> SDKEmbed completed an in-iframe authentication flowuser: { 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.

typescript
{
  type: "fanfest:async-flow-message",
  data: { type: MessageType, ...payload }
}

Message Types ​

TypeDirectionPurposePayload
validate-authentication:successAuth Iframe -> SDKSilent auth completed -- code and state availablecode, state, redirect_uri
validate-authentication:errorAuth Iframe -> SDKSilent auth failederror (string)

When the SDK receives a validate-authentication:success message, it:

  1. Calls POST /auth/silent-validation with the received code, state, and redirect URI
  2. Parses the response to extract user info and auth token
  3. Stores the auth state in SDKAuthState (backed by localStorage)
  4. Sends an authentication message to all active embed iframes
  5. 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 ​

  • SimpleObservableSubject holds the last emitted value (lastState). New subscribers receive this value immediately on subscribe (configurable via notifyOnSubscribe).
  • SimpleObservableValue extends Subject with a synchronous value getter/setter, used for simple reactive values like isAuthenticating.
  • ObservableDumbStorageValue wraps localStorage with a custom serializer (Zod-validated read, JSON write). This powers SDKAuthState, persisting auth tokens across page reloads.
  • Notification is encapsulated via a Symbol-keyed method. Only classes extending SimpleObservable can 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 IDs

Event 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 ​

AttributePurposeRequired
data-fanfest-trackAction ID or aliasYes
data-fanfest-onEvent trigger + modifiers (e.g., click:preventDefault)No (defaults to click)
data-fanfest-descriptionHuman-readable action descriptionNo
data-fanfest-experience-idGroups related actions into an experienceNo
data-fanfest-object-idExternal object identifier for deduplicationNo
data-fanfest-meta-*Custom metadata key-value pairsNo

Supported Events and Modifiers ​

Only three DOM events are supported: click, render, submit.

  • render events fire immediately when the element is first observed (no DOM event listener attached)
  • click and submit attach standard event listeners with configurable modifiers

Modifiers are appended with colons: data-fanfest-on="click:stop:preventDefault".

ModifierEffect
stopCalls event.stopPropagation()
preventDefaultCalls event.preventDefault()
captureRegisters listener in capture phase
onceRemoves listener after first invocation

Event Delivery ​

Tracked events flow through a pipeline:

DOM Event -> SDKTracker -> onTrackEvent observable -> SDK.trackEvent() -> LoyaltyService -> REST API

The LoyaltyService sends events to POST /public/external-actions/ingest with:

  • channelActionId: Resolved from the action registry (alias -> ID) or used as-is
  • x-app-id header: The SDK's clientId
  • authorization header: 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: fixed at top-left
  • Temporary: Created when login() is called, destroyed after auth completes (success or error)
  • No sandbox: The auth iframe does not use the sandbox attribute because it needs to navigate to external SSO providers and redirect back

Security Boundaries ​

BoundaryMechanism
SDK -> Embed communicationPostMessage with origin check against appOrigin
SDK -> Auth iframe communicationPostMessage with origin check against appOrigin
Embed iframe permissionssandbox attribute restricts capabilities
Auth token storagelocalStorage with Zod validation on read
API authenticationauthorization 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:

FormatOutput PathUse Case
IIFE{version}/fanfest-sdk.jsScript tag inclusion -- exposes window.FanFestSDK
ESM{version}/esm/fanfest-sdk.esm.jsES module import for bundled applications
CJS{version}/cjs/fanfest-sdk.cjs.jsCommonJS 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:

typescript
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:

ExportTypeDescription
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) => voidMap 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 from package.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 IIFE

Next Steps ​

Released under the MIT License.