Game lifecycle on filudi.com
A game on filudi.com is a static bundle in a sandboxed frame on a page of the hub. The SDK runs inside the frame; the hub runs outside. Everything between them is a message. This page says what happens, in order, and what it means for the code you write.
Where the bundle lives
Section titled “Where the bundle lives”Every version is unpacked under games/<slug>/<version>/ and served through the CDN from
https://<slug>.<play domain>/<slug>/<version>/index.html. One origin per game: your bundle shares nothing
with another game, not localStorage, not a cookie, not a cache. The host must match the path; the API
answers 404 for game A on the origin of game B. Until the play domain is switched on per game, the same
path is served from https://play.filudi.com.
The frame is sandbox="allow-scripts allow-same-origin allow-pointer-lock" with allow="fullscreen; autoplay". The page keeps the top-level navigation, the forms, the popups.
The handshake
Section titled “The handshake”- The hub opens the frame on the version’s URL.
- Your bundle calls
sdk.init({ gameId }). The SDK postsfiludi:readyto the parent and waits 1.5 s (handshakeTimeoutMs) for an answer. - The hub answers
filudi:init: the API URL, a game token, the slug of the game, the player, the locale, the site, the age policy, the consent state, the version string. The SDK accepts it only fromwindow.parent. init()resolves. From here the SDK calls the API itself with the game token: saves, scores, events.- Your game draws its first frame and calls
sdk.ready(). The SDK postsfiludi:loaded; the hub hides its loader and counts the first step of your funnel.
No answer within 1.5 s means the bundle is not in a Filudi frame: the SDK runs on the mock and
result.host says so. That is what happens in the preview, which runs at the top level.
// filudi:init, posted by the hub. The SDK reads it; the game never does.({ type: 'filudi:init', apiUrl: 'https://api.filudi.com', token: 'fg1.…', // game token, opaque, 24 hours gameId: 'gloop', player: { id: '…', nickname: 'Guest-Fox', guest: true }, locale: 'fr', site: 'filudi', policy: { personalizedAds: false, ageBand: 'all', accounts: true }, consent: { ads: false, analytics: true }, gameVersion: '1.4.0',});import type { FiludiInit } from '@filudi/game-sdk';
// filudi:init, posted by the hub. The SDK reads it; the game never does.const message: FiludiInit = { type: 'filudi:init', apiUrl: 'https://api.filudi.com', token: 'fg1.…', // game token, opaque, 24 hours gameId: 'gloop', player: { id: '…', nickname: 'Guest-Fox', guest: true }, locale: 'fr', site: 'filudi', policy: { personalizedAds: false, ageBand: 'all', accounts: true }, consent: { ads: false, analytics: true }, gameVersion: '1.4.0',};The game token
Section titled “The game token”The player signs in to filudi.com and has a token of their own. That token never enters your frame. The
hub exchanges it for a game token: limited to your game, to that player, for 24 hours. It opens your saves,
your boards and your events and nothing else. The hub sends a fresh one every 12 hours (filudi:token) and
the SDK uses it from the next request, without a reload.
When the API refuses the token, the SDK posts filudi:unauthorized, keeps saves in localStorage and
stops calling the API; the hub fetches a new token and reloads the frame. Your game sees a reload, nothing
else.
While the game runs
Section titled “While the game runs”The SDK posts to the hub: filudi:gameplay { running } on gameplay.start() and stop(), filudi:save { version } after a write, filudi:score { board, score, rank } after a submit, filudi:track { event }
after an event, filudi:fullscreen { request: true } on sdk.fullscreen(), filudi:ad to request an ad.
The hub treats each as a claim from an untrusted frame: it may update what the page shows for your game,
and nothing else.
The hub posts to the SDK: filudi:consent, filudi:locale, filudi:pause, filudi:resume,
filudi:token, and the answers to fullscreen and ad requests. They reach your game as the events of
host events, i18n and ads.
The CSP
Section titled “The CSP”The bundle route sends a Content Security Policy. Scripts, styles, images, media, fonts and workers load from
the bundle’s own origin only, with blob: and data: allowed for assets and wasm-unsafe-eval for
WebAssembly. fetch, XHR, WebSocket and EventSource reach the bundle’s origin and the API only. No frame
inside the frame, no form post. An inline <script>, an onclick="" attribute, eval, new Function and
setTimeout with a string throw. Inline styles work.
default-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; style-src 'self' 'unsafe-inline';img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self' data:;connect-src 'self' data: blob: https://api.filudi.com; worker-src 'self' blob:; frame-src 'none';object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors <site origins>default-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; style-src 'self' 'unsafe-inline';img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self' data:;connect-src 'self' data: blob: https://api.filudi.com; worker-src 'self' blob:; frame-src 'none';object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors <site origins>What this rules out: a CDN library, Google Fonts, an analytics script, an error reporter, a call to your own
server. Put fonts, audio and data in the bundle. Report through sdk.track(). The portal scripts of
CrazyGames, Poki and Playgama never run on filudi.com; the SDK loads them on their own portals.
What a game cannot do
Section titled “What a game cannot do”- Read the player’s own token, the page around it, or another game’s storage.
- Navigate the page, open a popup, submit a form.
- Load code or assets from another origin, or call one.
- Submit a score outside a play session, or replay one: scores are signed with a key the session gives.
- Send an event named
frame_loaded: the hub sends that one. - Go live by itself: staff activates every version.
Frame size and orientation
Section titled “Frame size and orientation”The frame takes the size the page gives it. Read innerWidth and innerHeight or a ResizeObserver, never
screen.width, and redraw on resize. The orientation you declare on the game (portrait, landscape,
any) tells the hub what to do on a phone held the other way: it shows a hint, your layout does not have to
cope. mobile says whether the game is listed on phones at all; minutes is the length of a typical
session, shown on the game’s page.
Audio starts after a gesture: the frame allows autoplay, browsers still block sound before a tap. A
pointerdown handler that calls preventDefault() keeps the frame from taking keyboard focus; call
canvas.focus() in it, with tabindex="0" on the canvas, or the arrow keys scroll the hub page.