# 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

```ts
// 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

```text
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

```json
{ "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:

```ts
// 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](/docs/builds#the-schema-hash).
