Build and deploy

Sites

How a build's web client is served from <project>.irt-serve.com and by irt dev, with caching, single-page app routes, cross-origin isolation and the reserved /api path.

A build can carry the project’s site: its web client’s static files. The live build’s site is served at https://<slug>.irt-serve.com, and irt dev serves it locally the same way, so a client always finds the room code it was built against.

Adding a site

// irt.config.ts
import { defineConfig } from "@irt/cli/config";

export default defineConfig({
  project: "space-race",
  room: "src/room.ts",
  site: { dir: "dist", build: "npm run build:web", crossOriginIsolated: false },
});

irt build, irt deploy and irt dev run site.build, if given, from the project’s directory, then take every file under site.dir. Use any bundler: Vite, esbuild, or none. A project without site deploys room code only.

How files are served

  • / and any path ending in / serve that directory’s index.html.
  • A path with no file extension that isn’t a file, such as /rooms/forest-7, serves /index.html, so a single-page app’s client side routes work. A missing path with an extension is a 404.
  • Only GET and HEAD; anything else is 405.
  • Content types come from the extension: HTML, JavaScript, CSS, JSON, source maps, images, fonts, audio, video, .wasm, and .glb and .gltf models. Other files are application/octet-stream. Every response has X-Content-Type-Options: nosniff.
  • HTML is served with Cache-Control: no-cache, so browsers check for a new version on every load; everything else with public, max-age=300. Give bundles hashed file names, as most bundlers do, so a new build’s HTML never loads an old build’s scripts.
  • When a build goes live, every edge server serves it within 10 seconds.

irt-serve.com is the user content domain: tenant content is never served from irt.io. Custom domains aren’t available yet.

Cookies and storage

Until irt-serve.com is on the Public Suffix List, browsers treat every *.irt-serve.com project as one site for cookies: another project’s page can set a cookie that yours receives. Name your own cookies with the __Host- prefix, which a sibling subdomain can’t set. localStorage and other storage are per origin, so they’re your project’s alone already.

Cross-origin isolation

site.crossOriginIsolated: true serves the site with

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

which make the page cross-origin isolated. That’s what SharedArrayBuffer, Wasm threads and precise timers need, such as for a physics engine running in workers. The cost: the page can then load a cross-origin resource only if it opts in, with Cross-Origin-Resource-Policy: cross-origin or CORS and a crossorigin attribute. Fonts, images and scripts from a CDN that doesn’t send those headers stop loading, and so do iframes and popups from other origins. Serve such files from the site itself, or leave the flag off.

The flag is part of the build, so it changes when a build goes live and rolls back with it.

/api is reserved

/api and every path under it answer 404 with

{ "error": { "code": "not_found", "message": "No backend is deployed for this project" } }

whatever files the site has. The path is kept for backends hosted on the site’s own origin, so don’t put site files under /api/; a file there is never served.

irt dev

irt dev serves the site and the local room host on one origin, http://127.0.0.1:7350 by default (--port changes it):

Path Serves
/v1/ws The client protocol over WebSocket
/v1/rooms, /v1/codes/... The client HTTP API that createRoom, joinByCode and listPublic use
/api, /api/... 404, as deployed
anything else The site, as deployed: index.html fallback and COOP and COEP when the build asks for them

Locally every response is Cache-Control: no-store, so a rebuild shows on the next reload. irt dev reruns site.build and restarts the room host when a project file changes; connected clients reconnect. Paths under /v1/ belong to the room host, so keep site files out of a v1 directory.

Because both are on one origin, a client can pick its router by where it’s served from:

// router.ts
export const router = location.hostname.endsWith(".irt-serve.com") ? "wss://eu.irt.io" : location.origin;

irt deploy prints the router the project’s clients use.

Common mistakes

  • Hard-coding http://127.0.0.1:7350 as the router. The deployed site then can’t connect. Choose by location, as above.
  • Turning on crossOriginIsolated with assets on another origin. They stop loading unless they send CORP or CORS headers.
  • Bundles without hashed names. They’re cached for 5 minutes, so a page can mix a new index.html with old scripts.
  • Files under /api/. They’re never served.
  • Deploying the site separately from the room. Sites deploy only with their build, which is what keeps clients and rooms on one schema; see Builds and deploys.

View this page as Markdown