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.