Skip to content

CORS and origins

A browser call reaches the API only from an origin the API allows. The same list is the frame-ancestors of every game bundle: an origin that may call the API may frame a game, and no other.

The allowed origins are the union of:

  • https://<domain> for every entry of domains of every approved site. A pending site has no origin, and a proposed domain (pendingDomains) joins the list once staff approves it.
  • The hub origins the API is started with (HUB_URLS): filudi.com, the playground and the local ports of the monorepo, http://localhost:4321 and http://localhost:5173.
  • The console origins (CONSOLE_URLS), for /v1/console/* and /v1/admin/* only, with credentials.

Each API pod reloads the site domains every 60 seconds. A domain approved in the back office works within a minute, on every pod, without a redeploy.

localhost is never a site domain. For local work, run the API from the monorepo, which allows the two local ports above, or add your port to HUB_URLS on that API.

/v1/console/* and /v1/admin/* answer the console origins with Access-Control-Allow-Credentials: true, so the filudi_console cookie travels with credentials: 'include'. Every non-GET request there must carry an Origin from that list; otherwise 403 forbidden. A browser sets Origin on its own. A curl adds it by hand.

Each live game runs on its own origin, https://<slug>.<play domain>. That origin gets CORS on the five routes a game token opens (GET /v1/sdk/players/me, /v1/sdk/saves/{gameId}, POST /v1/sdk/sessions, /v1/sdk/leaderboards/{gameId}/*, POST /v1/sdk/events) and on nothing else. A game cannot read a fresh guest token, a sign-in answer, a rating or a feedback route from its frame.

A bundle answers with a Content Security Policy that lets it fetch its own origin and the API only, with no remote script and no frame. frame-ancestors lists the allowed origins, so your page frames a game only once your domain is approved.

/v1/server/* sends no CORS headers at all. A browser cannot call it, which is the point: the server key stays in your backend. Call these routes from your servers and hand the browser a player token (POST /v1/server/players/{playerId}/tokens) or a game token (POST /v1/server/game-tokens) over your own authenticated channel.

Your pages send a CSP of their own. The one that lets a frame load and the page call the API:

frame-src https://play.filudi.com https://*.<play domain>
connect-src 'self' https://api.filudi.com
img-src 'self' data: https://play.filudi.com
media-src https://play.filudi.com

Never serve a bundle from your own origin: the frame has allow-same-origin, so a bundle on your origin could read your localStorage, where the player’s token sits. GameFrame throws on a bundle from the page’s origin.