multiplayer-setup.md - Virtastic Notepad

OpenMW manual: Multiplayer setup: SSO and storage

--- title: "Multiplayer setup: SSO and storage" description: "Provision what only an operator can for a multiplayer server: the OAuth app, optional S3-compatible storage, the vanilla manifest, and the config that turns them on." book: "Self-hosting" order: 51 --- This page covers the provider-side steps for a multiplayer server: creating the OAuth app, choosing storage for the locker, generating the upload manifest, and the configuration that turns it all on. > Choose **Multiplayer** in the setup wizard; nothing needs enabling first. Finishing the > wizard restarts the container as the multiplayer server, whose dashboard is where the rest > of this guide happens. Everything in this guide assumes you have done that; see > [Self-hosting](/openmw/docs/self-hosting/#quick-start) for the full note. > **Running the Docker stack?** Then most of this is already done for you. The setup wizard > asks which sign-in methods you want and takes the OAuth client ID and secret, and the S3 > endpoint, bucket and keys, in the browser, and writes the configuration itself. What it > cannot do is create the OAuth app at Google, Discord or Microsoft, or create the bucket: > those are §1 and §2 below, and the wizard shows you the exact redirect URL to paste while > you are there. Read §4 only if you are configuring a server by hand. > > The stack runs `ghcr.io/virtastic/openmw-web-server:<tag>`, the multiplayer-capable image > (server + headless OpenMW sim peer) every release publishes, so the ~25-minute engine > compile never happens on your box. Updates pull the next tag (`./setup.sh --update` or the > dashboard's Update button); rollback is `OPENMW_WEB_TAG=<previous tag>` in `.env`, then > `docker compose pull && docker compose up -d` - see > [Self-hosting: Updates](/openmw/docs/self-hosting/#updates). Players' game data lives in the **storage locker**: an S3-compatible bucket, or the server's own disk when no bucket is configured. This guide covers the things only an operator can provision (the OAuth app, optionally the bucket, and the upload manifest), then the config that turns it all on. The rest of the server walkthrough (game data for the sim peer, running it, the reverse proxy) is in [Self-hosting: Multiplayer server](/openmw/docs/self-hosting/#multiplayer-server), and the no-Docker path in [Advanced hosting](/openmw/docs/advanced/). Sign-in is a **choice**, not a fixed rule: single sign-on, a username and password, or both, and one account can use either. The hosted service sets `requireSso = true` because it does not want to hold passwords; a server you run decides for itself, in the wizard. The OAuth app below is the one hard prerequisite nobody can create for you. Storage is optional: with no S3 configured the server stores lockers and savegames on its own disk (§2).

## 1. Google OAuth app (≈5 minutes)

1. Go to <https://console.cloud.google.com/> → create a project (or pick one). 2. **APIs & Services → OAuth consent screen** → External → fill the app name + your email → Save. You do **not** need to submit for verification for personal/testing use; add your own Google account under **Test users**. - Scopes: leave default. We request `openid profile email`; accounts are keyed on the provider's stable `sub`, never the email, and only provider-verified addresses are used. 3. **APIs & Services → Credentials → Create Credentials → OAuth client ID**: - Application type: **Web application** - Authorized redirect URI (exactly, no trailing slash): - Local test: `http://127.0.0.1:8080/auth/google/callback` - Production: `https://YOUR-SERVER-HOST/auth/google/callback` 4. Copy the **Client ID** and **Client secret**. You now have `clientId` + `clientSecret` for the config below. > Discord and Microsoft work identically (`[auth.discord]`, `[auth.microsoft]`); their > redirect URIs use `/auth/discord/callback` etc. Google is the example throughout.

## 2. Storage: a bucket, or this server's own disk

**You can skip this section.** With no `endpoint`/`bucket`/keys the locker now stores everything in `<dataDir>/locker-blobs` on the server instead of going inert. Set one thing if you do: ```toml [locker] publicBase = "https://mp.example.com" # the origin PLAYERS reach this server on ``` Upload and download URLs are built from it (they carry a signed, expiring token in the path rather than an S3 signature). Leave it empty and the server logs `locker.no_public_base` and falls back to localhost, which works only when the player is on this machine. The trade is bandwidth and disk: every player's library and savegames sit on this box and stream out of it. A bucket is still the better answer above a handful of players.

### S3-compatible bucket (Cloudflare R2 recommended — no egress fees)

Any S3 API works (AWS S3, Cloudflare R2, Backblaze B2, MinIO). **R2 is the best fit** — no per-GB egress charge, which matters because players stream their data every session. **Cloudflare R2:** 1. Cloudflare dashboard → **R2 → Create bucket** (e.g. `omw-lockers`). Note your **Account ID** (top of the R2 page). 2. **R2 → Manage R2 API Tokens → Create API Token**: - Permissions: **Object Read & Write** - Bucket: the one you made - Create → copy the **Access Key ID** and **Secret Access Key** (shown once). 3. Your endpoint is `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`, region `auto`. 4. **CORS** (so the browser can PUT/GET directly). In the bucket → Settings → CORS policy: ```json [{ "AllowedOrigins": ["http://127.0.0.1:8910", "https://YOUR-GAME-HOST"], "AllowedMethods": ["GET", "PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["Content-Range", "Content-Length"] }] ``` `Content-Range` must be exposed — the game streams via HTTP Range requests. **AWS S3** is the same shape: bucket + an IAM user with `s3:GetObject`/`s3:PutObject`/ `s3:ListBucket`/`s3:DeleteObject` on `arn:aws:s3:::your-bucket/*`, endpoint `https://s3.<region>.amazonaws.com`, real region, plus the same CORS rule on the bucket.

## 3. The vanilla-manifest (so the locker accepts your retail files)

The locker only accepts recognized game files — that is what keeps it a backup locker and not general file hosting. Generate the manifest from your own legal copy: ```bash node server/tools/gen-vanilla-manifest.mjs "/path/to/Morrowind/Data Files" \ --out server/devdata/vanilla-manifest.json ``` Then place `vanilla-manifest.json` in the server's **shared dir** (`--shared`, or `--data` for a single-world server). Until it exists the locker refuses every upload — the safe default. **Different distributions still connect.** Steam, GOG, disc, and localized copies of `Morrowind.esm`/`.bsa` differ byte-for-byte, so an exact-hash gate built from *your* copy would reject a friend's legitimate copy from a different store. So the locker accepts a file when either its exact sha256 is known **or** its filename matches a manifest entry and its size is within ±5% (a movie renamed to `Morrowind.esm` is nowhere near ~79.8MB, so it is still refused). A refusal is logged (`locker.refused_unrecognized`) with the offered name, size and hash, so an operator can add a genuinely new distribution to the manifest by hand. Unknown hashes that pass on name+size are deliberately NOT remembered — learning them would let one uploader whitelist a byte-mismatched file for everyone. To require exact hashes only, set `acceptByNameAndSize = false` under `[locker]` in the config. > The multiplayer content gate is name-based, so same-named files from different stores play > together fine; only genuinely different *records* (e.g. a different language ESM) would > desync, which is inherent to any content-sync system.

## 4. Config — turn it on

In your server's `config.toml` (in the data dir): ```toml [auth] requireSso = true # or false to also allow passwords returnUrl = "http://127.0.0.1:8910/launcher.html" # your game/launcher URL [auth.google] enabled = true clientId = "…apps.googleusercontent.com" clientSecret = "…" redirectUri = "http://127.0.0.1:8080/auth/google/callback" [locker] endpoint = "https://<ACCOUNT_ID>.r2.cloudflarestorage.com" region = "auto" bucket = "omw-lockers" ```

### If you are behind Cloudflare

```toml [limits] trustCloudflareIp = true ``` Only set this when Cloudflare really does terminate in front of the server **and** your edge deletes any client-supplied `CF-Connecting-IP` (the shipped [`deploy/Caddyfile`](https://github.com/Virtastic/openmw-web/blob/main/deploy/Caddyfile) and `deploy/openmw-mp.caddy` both do). It is wrong in both directions and both are quiet: - **On, with an edge that does not strip it**, any client can name its own address. It resets its own login rate limit, walks past an IP ban and past the per-address connection cap, and can pin its failed sign-ins on somebody else. - **Off, while behind Cloudflare**, every player resolves to the Cloudflare edge address. Per-IP limits then apply to the whole player base at once, so `loginPerMinPerIp = 5` means the sixth person to sign in in any given minute is refused. The server logs which mode it chose at boot (`net.client_ip_mode`), and warns once (`net.cloudflare_header_ignored`) if the header shows up while the setting is off. S3 keys go in the **environment**, not the config file: ```bash export S3_ACCESS_KEY_ID=… export S3_SECRET_ACCESS_KEY=… node server/dist/server.mjs --data /path/to/data --port 8080 ``` Sanity checks once it's running: ```bash curl -s localhost:8080/auth/providers # -> {"providers":["google"],"allowPasswordLogin":false,…} curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/locker/files # 401 (needs a token), NOT 503 ``` `503` from `/locker/files` should no longer happen: without S3 the locker falls back to the server's disk. If you see one, storage failed to initialise — check the boot log for `locker.filesystem_storage` or `s3.configured`. Savegames ride on the same storage and the same session token: ```bash curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/saves # 401 (needs a token) ``` They are capped separately from the game-data library (`[locker] maxSaveBytesPerAccount`, 512 MiB by default) so a full library can never be the reason a player cannot save.

## 5. Play

Open the launcher on your server's origin → **Multiplayer** tile → it sends you through your provider's sign-in → you pick a public handle → back to the game. There is no server address to type: the launcher talks to the server it was served from. First time, the upload wizard asks for your Data Files folder once, hashes and uploads it to your locker, then streams it back. After that, any device: sign in, and your data (and your character) are already there. For a two-player local test, use two browser profiles (each is one account).

## Production notes

- **HTTPS is required in production** for both hosts — OAuth redirect URIs and the locker's cross-origin fetches need it, and the SSO ticket is a credential. - Rotate the R2 token if it ever leaks; the game keys are per-session and short-lived. - [Legal posture](/openmw/docs/legal/) covers the DMCA-agent registration and the storage-locker invariants that must not be relaxed.