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:
| Part | Role | Exposed externally? |
|---|---|---|
| api | REST, WebSocket, and the realtime surface; all business logic | Only through the gateway |
| worker | Background jobs: event queue (outbox), webhook delivery, scheduled jobs, ranking projections | No |
| PostgreSQL | Persistent data | No |
| Redis | Rate limits, CSRF usage records, realtime keys | No |
| converter | 3D model conversion (amd64 only) | No |
| public-gateway | The Caddy gateway opened to players and to the game server; cuts /v1/panel/* off with 404 | Yes (the layer in front terminates TLS) |
| admin-gateway | The admin panel | Loopback only by default |
| grpc-proxy | gRPC for trusted services | Loopback 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 inAccess-Control-Allow-Originand addsAccess-Control-Allow-Credentials: true;*is never used and unknown origins get no headers. - A preflight (
OPTIONS) returns204for an allowed origin and403for a disallowed one;Access-Control-Max-Ageis 600 s. - In non-production environments (
CRX_ENVdevelopment or test), iflocalhostor127.0.0.1is in the list,http://localhost:<port>andhttp://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:Originis 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 examplegame.example.comandopen-play.other.dev), the browser does not attach the session cookie to cross-sitefetchrequests; even with correct CORS headers a session cannot be established. Subdomains of the same registrable domain (for examplegame.example.comandopen-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:
| Setting | Value |
|---|---|
TypeScript baseUrl | https://game.example.com/open-play (the SDKs accept a path prefix), or window.location.origin if you do not use a prefix |
Unity BaseUrl | the 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-env | the 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’sreverse_proxydoes this on its own; on nginx, setproxy_http_version 1.1and theUpgradeheaders explicitly. - The
OriginandCookieheaders must be forwarded as they are. - Client IP: set the
X-Forwarded-Forheader 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 getting429at the same time. Resolve this with your operator team (see Rate limits). - Do not open the panel path. The
/v1/panel/*path is404on the public-gateway; do not give the panel a route through your game domain.
Cookie behavior
| Cookie | Purpose | Attributes |
|---|---|---|
__Secure-crx.session | Player session | HttpOnly; Secure; SameSite=Lax; Path=/ |
__Secure-crx.refresh | Session refresh | same |
__Secure-crx.csrf | CSRF double-submit cookie | same; 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 ascurl) are not caught by the origin check; if they use cookies, the CSRF double-submit protection checks them. - If
Sec-Fetch-Site: cross-siteandSec-Fetch-Mode: navigatearrive 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
Originderived from configuration on ticket and WebSocket requests (the scheme, domain, and non-default port ofBaseUrl; can be changed withNativeOrigin). 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.