Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture and hosting

This page explains how to position your game with Open Play: the parts of the stack, how browser games call Open Play directly (CORS) or through a reverse proxy, binding a domain to an environment, and cookie/TLS requirements. If you will publish a browser or WebGL game, read this page from start to finish.

What runs where

The reference deployment is a Docker Compose stack:

PartRoleExposed externally?
apiREST, WebSocket, and the realtime surface; all business logicOnly through the gateway
workerBackground jobs: event queue (outbox), webhook delivery, scheduled jobs, ranking projectionsNo
PostgreSQLPersistent dataNo
RedisRate limits, CSRF usage records, realtime keysNo
converter3D model conversion (amd64 only)No
public-gatewayThe Caddy gateway opened to players and to the game server; cuts /v1/panel/* off with 404Yes (the layer in front terminates TLS)
admin-gatewayThe admin panelLoopback only by default
grpc-proxygRPC for trusted servicesLoopback only by default
Player browser ──HTTPS──▶ game.example.com ─┬─ /            ▶ your game's static files
                                            └─ /open-play/* ▶ Open Play public-gateway ▶ api
Your game server ───────HTTPS───────────────────────────────▶ Open Play public-gateway ▶ api
Open Play worker ──signed webhook───────────────────────────▶ your server

Public TLS is not terminated inside this stack; you need Cloudflare, a load balancer, or a Caddy/nginx entry point. Because the panel cookie is Secure, the panel also requires HTTPS. For installation, resource budgets, and network details, see the production deployment guide (docs/production-and-adapters.tr.md).

Note

The gRPC interface is not a public channel for game developers; by default it listens on loopback only and the SDKs do not use it.

Calling from the browser: direct CORS or reverse proxy

If your game’s domain is in the environment’s domain list, your browser game can call Open Play directly. On the public API (panel and setup endpoints excluded) the server sends CORS (cross-origin resource sharing) headers. The allowed set is the union of the domains written to all environments under Game settings › Environments and domains in the panel and the operator’s global allowed list (CRX_AUTH_TRUSTED_ORIGINS); there is no game-specific setting. For example, if the game runs at https://game.example.com, Open Play at https://open-play.example.com, and game.example.com is an environment domain, the game talks to it directly with baseUrl: "https://open-play.example.com/open-play".

Rules:

  • The allowed origin is exactly https://<domain> (no path, no non-default port). The response reflects that origin verbatim in Access-Control-Allow-Origin and adds Access-Control-Allow-Credentials: true; * is never used and unknown origins get no headers.
  • A preflight (OPTIONS) returns 204 for an allowed origin and 403 for a disallowed one; Access-Control-Max-Age is 600 s.
  • In non-production environments (CRX_ENV development or test), if localhost or 127.0.0.1 is in the list, http://localhost:<port> and http://127.0.0.1:<port> are allowed too.
  • A domain newly added in the panel takes effect within a few seconds (the server snapshot refreshes every 5 s).
  • CORS is not authorization. The Origin/CSRF/environment checks run unchanged; CORS only lets the browser read the response. The panel path (/v1/panel/*) never gets CORS headers. The realtime WebSocket handshake is unchanged: Origin is checked per environment.

Warning

The session cookie needs the same site. Cookies are written with __Secure-, HttpOnly, SameSite=Lax. If the game is on a different registrable domain than Open Play (for example game.example.com and open-play.other.dev), the browser does not attach the session cookie to cross-site fetch requests; even with correct CORS headers a session cannot be established. Subdomains of the same registrable domain (for example game.example.com and open-play.example.com) are the same site and work. A different site is not supported; use the reverse proxy below in that case.

A reverse proxy is still an option. If you want to serve the game and the API from the same origin (or the game is on a different site), set up a reverse proxy on the game’s own gateway that routes the API path to Open Play; CORS then does not come into play. Example (Caddy):

game.example.com {
  # Strips the /open-play/ prefix and forwards to the Open Play public-gateway.
  handle_path /open-play/* {
    reverse_proxy open-play-gateway:8080
  }
  # Everything else is your game's static files.
  handle {
    root * /srv/game
    try_files {path} /index.html
    file_server
  }
}

With a reverse proxy, the client-side configuration is:

SettingValue
TypeScript baseUrlhttps://game.example.com/open-play (the SDKs accept a path prefix), or window.location.origin if you do not use a prefix
Unity BaseUrlthe same address; today the Unity WebGL SDK requires the same origin as the page, otherwise you get the webgl_cross_origin_unsupported configuration error (server CORS does not lift this; see Unity SDK)
x-crx-envthe environment ID (the SDKs send it from the environmentId setting)

Watch out for these on the reverse proxy:

  • The WebSocket upgrade (Upgrade/Connection) must pass through; the realtime connection is on /v1/realtime/ws. Caddy’s reverse_proxy does this on its own; on nginx, set proxy_http_version 1.1 and the Upgrade headers explicitly.
  • The Origin and Cookie headers must be forwarded as they are.
  • Client IP: set the X-Forwarded-For header correctly. If there is an extra proxy in front and the proxy addresses the server should trust have not been declared, all players fall into a single IP bucket; the symptom is different players getting 429 at the same time. Resolve this with your operator team (see Rate limits).
  • Do not open the panel path. The /v1/panel/* path is 404 on the public-gateway; do not give the panel a route through your game domain.
CookiePurposeAttributes
__Secure-crx.sessionPlayer sessionHttpOnly; Secure; SameSite=Lax; Path=/
__Secure-crx.refreshSession refreshsame
__Secure-crx.csrfCSRF double-submit cookiesame; 15-minute lifetime

Secure cookies require HTTPS. In local development, the SDKs allow plain http only on loopback addresses (localhost, 127.0.0.1, ::1); check whether your browser accepts Secure cookies for localhost, and if it does not, use HTTPS locally as well.

Domains and trusted origins

The browser adds an Origin header to state-changing (POST, PUT, PATCH, DELETE) requests. The server checks whether this origin is a domain bound to the relevant environment:

  • Domain bound to the environment: the names you enter in the panel under Game settings › Environments and domains › Domains. Each line is one full domain (for example game.example.com); there are no wildcards, IPs, or ports; at most 10 domains can be bound per environment. The same domain cannot be bound to two environments (409).
  • Global allowed list: the operator’s server setting (CRX_AUTH_TRUSTED_ORIGINS: a full domain or *.domain). If the environment has no domain of its own, this list applies.
  • If a domain is bound to another environment, a browser coming from that domain cannot write to this environment and cannot read this environment (403).
  • Clients that do not send Origin (your game server, non-browser clients such as curl) are not caught by the origin check; if they use cookies, the CSRF double-submit protection checks them.
  • If Sec-Fetch-Site: cross-site and Sec-Fetch-Mode: navigate arrive together, the state-changing request is rejected.

The realtime ticket is also bound to the same origin rule: a ticket is single-use and bound to the origin it was obtained from.

Note

Unity native builds send a fixed Origin derived from configuration on ticket and WebSocket requests (the scheme, domain, and non-default port of BaseUrl; can be changed with NativeOrigin). This origin must be in the environment’s domains or in the global allowed list. See Unity SDK.

Domain binding steps: Environments and domains.

Calling from your game server

Your game server connects directly (HTTPS) to Open Play with a server key. No reverse proxy, CORS, or domain binding is needed (CORS concerns only the browser); the environment comes from the key. Keep the key only in the server environment (an environment variable or secret management).

To verify a player’s identity on your server, you can verify the access JWT offline: Server-side verification.

Webhooks (Open Play → your server)

The worker sends events to your address as signed HTTPS requests. Your address must be publicly reachable and must verify the signature. See Webhooks and Security.

Pre-release check

The Production checklist gathers the items for domain binding, key management, TLS, and monitoring.