Onboarding¶
What otomo is¶
Otomo is a game-agnostic backend framework for online games. It provides the identity, content delivery, session, administration and edge pieces a multiplayer game needs: a set of Go 1.27.1 services, a staff admin UI, an allocator for game servers, and a Docker Compose deployment that runs on one Linux host. Otomo contains no game content and depends on no particular game. Your game plugs into its HTTP contracts and supplies its own client and game servers.
Architecture¶
Internet
│
▼
Edge (nginx :80/:443)
TLS, ACME; /docs/ → technical wiki,
/ → game site, player API → gateway
│
┌────────────────────┴────────────────────┐
▼ ▼
Godot client Ionic admin UI
(player) (staff)
│ │
▼ ▼
Gateway (player edge, public) Gateway (dev, admin edge, private)
│ │
└───────────────────┬──────────────────────┘
▼
Essential services
Go 1.27.1: Auth, Config, Patch, Session,
Dashboard, admin-auth
│
┌─────────────┴─────────────┐
▼ ▼
Allocator Data layer
(internal only; reserves Postgres, Valkey,
servers, issues tickets) blob storage
▲
│ register + heartbeat
Game servers (headless)
│
▼
Gameplay Proxy (public UDP 27000)
The Gameplay Proxy carries player connections to the game servers; clients never reach a game server directly. See the launch hand-off for the launch path.
Repository layout¶
| Path | What is in it |
|---|---|
services/ |
One directory per service. A Go service holds its module, Dockerfile and migrations/; the admin UI and wiki have their own build layouts. |
deploy/ |
compose.yaml, the provisioning and release scripts, the Postgres init script, the edge runbook, observability configs and generated secrets. |
ci/ |
Jenkins pipeline files: ci/services/Jenkinsfile, one unit_test.sh per service, the dispatcher, and the CI Compose stack. |
docs/ |
The source for this technical wiki, including docs/guide/. |
.github/workflows/ci.yml |
GitHub Actions workflows for gateway, gateway_dev and adminui. |
Two more directories sit under services/: matchmaker, still a placeholder that is not in
the running stack, and cli.
Services¶
| Service | What it does | Design doc |
|---|---|---|
edge |
nginx on :80/:443; TLS termination, ACME, routes the player API to gateway, /docs/ to the technical wiki and / to the game site |
Allocator and edge plan, wiki service |
gateway |
Player edge: routing, token validation, per-IP rate limiting | Gateway techspec |
gateway_dev |
Staff/admin edge: the same job for the private control plane | Gateway techspec, common stack |
auth |
Player identity: device login, refresh rotation, JWKS | Auth identity contract, Auth techspec |
admin_auth |
Staff identity: login, MFA, refresh cookies, user administration, JWKS | Auth identity contract |
config |
Author, validate, version and publish config and content | Config |
patch |
Serve published manifests and content blobs to clients | Patch (minimal) |
session |
Profiles, presence, friends, parties (lobbies) and events | Session (minimal) |
allocator |
Internal only; reserves a game server and issues single-use join tickets | Allocator and edge plan, launch hand-off |
dashboard |
Staff metrics, logs and audit trail | Dashboard |
adminui |
Ionic staff SPA hosting the Config and Dashboard modules at /admin/ |
Common stack |
wiki |
MkDocs Material builder and runtime for the technical wiki and a game site | Wiki service |
How a change flows¶
- Branch off
staging, namedSCRUM-<n>-short-description. - Commit and open a pull request into
staging. - CI runs:
- GitHub Actions (
.github/workflows/ci.yml) runs on any push or pull request that touchesservices/gateway/,services/gateway_dev/orservices/adminui/. - Jenkins runs the per-service jobs (
otomo-<service>), all drivingci/services/Jenkinsfile. Jenkins is CI forstagingonly and no longer deploys. - Merge into
staging. - Deploy manually, one service at a time, by image tag:
deploy/scripts/release.sh auth # tag = short git SHA of the working tree
deploy/scripts/release.sh gateway 1.4.0 # or an explicit tag
release.sh builds 127.0.0.1:5000/otomo-<service>:<tag>, pushes it, writes that tag into
deploy/.env and recreates the service. For a service with a *-migrate one-shot (for
example auth), the recreate also runs its migration.
Rolling back is a re-point, not a rebuild:
sed -i 's/^OTOMO_AUTH_TAG=.*/OTOMO_AUTH_TAG=<previous sha>/' deploy/.env
cd deploy && docker compose --env-file .env -f compose.yaml up -d auth
Tags are immutable and the registry is not pruned, so the previous image is still there. A rollback of a service with migrations needs a schema that is compatible in both directions, because goose does not undo a version on its own.
Local development¶
- The toolchain is Go 1.27.1, pinned exactly. Every
go.modcarriesgo 1.27.1andtoolchain go1.27.1; build and test with that version. - Run a service's tests from its directory:
- DB-backed tests need the CI database variables, named
<SERVICE>_TEST_DATABASE_URL(for exampleSESSION_TEST_DATABASE_URL). CI provides Postgres and Valkey inci/compose.yaml. - Run one CI test level locally the way Jenkins does:
docker compose -f ci/compose.yaml run --rm --workdir /src go \
sh ci/services/otomo-<service>/unit_test.sh sanity
docker compose -f ci/compose.yaml down -v
Use node instead of go for adminui, and add --no-deps for a service that needs no
database. See ci/services/README.md.
Conventions¶
| Area | Convention |
|---|---|
| Error body | {"error":{"code":"...","message":"...","request_id":"..."}} for every 4xx and 5xx (COM-5) |
| Request IDs | Gateway adds X-Request-Id when absent; every service logs it and forwards it on outbound calls (COM-3) |
| Health endpoints | /healthz, /readyz and /metrics are served on the internal :9090 listener, never on the public port |
| Images | Multi-stage builds, runtime image gcr.io/distroless/static-debian12:nonroot; non-root, no shell, so no Compose healthchecks and nothing to docker compose exec |
| Secrets | Never in the repository; generated into deploy/.env and deploy/secrets/ at deploy time (COM-9) |
| Logging | log/slog JSON on stdout |
| Metrics | Names <service>_<thing>_<unit>; no player IDs, staff IDs, raw URLs or request IDs as labels |
A 503 from either gateway's /readyz means one token domain's JWKS is not fetching, not
that the stack is down. Both gateways wait on two domains.
Security rules¶
- No otomo cookie sets
Domain. Login-session cookies use the__Host-prefix. The public edge stripsCookieandSet-Cookieon every public route, and HSTS is sent for this host only, never withincludeSubDomainsorpreload. The parent domain is shared with other teams. See the auth identity contract. - The Let's Encrypt quota is shared. The certificate authority issues at most 50 new
certificates per week for the whole parent domain. Rehearse with
dry-run(staging CA, no quota) every time and request the real certificate exactly once per host name. The edge container never requests certificates itself. See the Allocator and edge plan anddeploy/edge/README.md. - No internal route is reachable through either gateway. Internal calls carry a
per-caller service key, checked on the callee's internal listener only. Never trust
X-Forwarded-Forunless the peer is in the gateway's configured trusted-proxy list.
Where to go next¶
| If you want to | Read |
|---|---|
| Understand the whole M1 stack | M1 overview, common stack |
| Work on routing, rate limits or the edge | Gateway techspec, Allocator and edge plan |
| Work on identity | Auth identity contract, Auth techspec |
| Work on content | Config, Patch (minimal) |
| Work on sessions and lobbies | Session (minimal), player plane plan |
| Work on game-server launch | Launch hand-off |
| Build the game client | Godot SDK guide |
| Run or edit a wiki or site | Wikis and sites, wiki service |
| Deploy otomo for your game | Setting up otomo for your game |