Games & apps¶
The catalogs themselves are generated — Games and Apps list every entry, including the unlisted ones. This page explains how a game or app is wired into the site, which is the part that isn’t obvious from a catalog.
The catalog is the source of truth¶
lib/games.ts and lib/apps.ts are the single source of truth for what appears on the site. The homepage sections and the /games and /apps browse pages all read from them; nothing hardcodes a card. An entry carries its route, description, tags, status, whether it needs sign-in (authGate), and whether it is unlisted.
Adding a game means adding a catalog entry and a route. Adding only the route produces a game that exists but that nothing links to.
Anatomy of a game¶
A typical game spans four places:
Layer |
Location |
|---|---|
Route |
|
Components |
|
Client logic |
|
Server (multiplayer only) |
|
Full-screen apps share a chrome tier in components/shared/: AppShell, AppHeader, AppToaster, ConnectionStatus, and the --app-* token contract in app-theme.css. Use them rather than rebuilding a header — ConnectionStatus in particular already handles the reconnect banner and the peer-wait overlay, which are easy to get subtly wrong.
Realtime¶
Multiplayer games talk to Node hubs over Socket.io, not to the web tier:
Hub |
Port |
Serves |
|---|---|---|
|
7001 |
The general hub, including RMHMusic |
|
7676 |
RMHBox party games |
|
7003 |
Watch-party sync |
Every client socket is built on lib/shared/realtime/client.ts — the factory that handles fast reconnect, wake signals, per-attempt auth and an opt-in outbox. Event names belong in lib/<app>/events.ts so client and server import the same constants instead of trading string literals.
Important
State that decides an outcome — scores, turn order, who won — must be authoritative on the server. The client renders it; it does not own it.
PEER_GRACE_MS in lib/shared/realtime/types.ts (15s) is read by both the client and the server’s PresenceGrace, so the two cannot disagree about when a peer has actually left rather than briefly dropped.
Audio, 3D and performance¶
Audio:
howler/tone. Always obtain a context viagetAudioContext()fromlib/shared/platform.ts— nevernew AudioContext()— so the page doesn’t end up with several competing contexts.3D:
@react-three/fiberwith@react-three/rapierfor physics.lib/performance-tier.tsandlib/motion-features.tsscale effects to the device. RespectuseReducedMotion— the globalMotionConfigalready setsreducedMotion="user", so framer-motion animations honour it, but imperative animation loops need to check.
Cross-cutting surfaces¶
Route |
What it is |
|---|---|
|
Browse pages, rendered from the catalogs. |
|
Arcade Pass — per-game daily challenges ( |
|
Cross-game saves. |
|
A saved replay. |
|
The ranked ladder. |
Leaderboards are served by lib/leaderboard.server.ts and exposed publicly through the API (read:leaderboards) at /api/v1/leaderboards/{game} — so a game’s leaderboard is readable programmatically without scraping the page.
Unlisted games¶
Some games are reachable by URL but deliberately absent from the browse pages — the secret/* routes among them. They are marked unlisted in the catalog and listed in a dedicated section of the generated game catalog. Unlisted is a discovery decision, not a security boundary: an unlisted game with an auth gate is protected by the gate, not by being hard to find.