Build targets
The same game ships as more than one build. Most of the code is shared; a build differs in three
places only: the backend that owns the garage, the ad SDK, and the features that need the race
server. Plans: docs/2026-11-12-web-distribution.md and docs/2026-11-12-web-builds-phase1-plan.md.
Targets
Section titled “Targets”VITE_TARGET picks the build, and core/target.ts turns it into flags:
| Target | Build | online |
purchases |
ads |
|---|---|---|---|---|
app |
npm run build:native (iOS, Android) |
yes | yes | admob |
web |
npm run build (play.nitrolegend.com, installable PWA) |
yes | no | none |
portal |
npm run build:portal (web portals) |
no | no | none |
playables |
not wired yet (YouTube Playables) | no | no | none |
vite/config.prod.mjs sets the target: app when CAPACITOR is set, else web, unless
VITE_TARGET names another. Only web gets the service worker; app ships MP4 videos only;
portal and playables build into their own folder (dist-portal, dist-playables). The dev server
runs as web, so a debug app on a phone with live reload needs VITE_TARGET=app on its dev server,
or it has no ads and no gold packs: start Vite with it, adb reverse its port and the race server’s,
then npx cap run android --live-reload --host localhost --port <vite port>. Sign-in on such a build
uses the debug key’s Android OAuth client; its purchases go to RevenueCat’s Test Store.
The target is replaced with a literal at build time, so TARGET is a plain object of constants and
Vite drops whatever a build cannot reach: the unused backend, the unused ad SDK and every
if (TARGET.online) branch. No Vite alias or per-target file list is needed. To check what a build
holds, grep its bundle (grep -l '/api/garage' dist-portal/assets/*.js).
Backend
Section titled “Backend”Everything that changes the garage or the career goes through a backend, in two halves:
-
player/backend/backend.ts(interface inplayer/backend/types.ts): load the garage, a shop’sGarageActionand ad rewards. -
race/judge/raceBackend.ts(interface inrace/judge/types.ts): career, quick and weekly race tickets and their judging, the double prize and keeping boss rounds. It sits in theracemodule because judging replays the race (race/replayEventRace.ts), whichplayermay not import. -
Remote (
player/backend/remote.ts, whenonline): the race server owns the garage, as Architecture describes. The calls live inplayer/account/. -
Local (
player/backend/local.ts, without the server): the game applies the server’s own rules (applyGarageAction,player/garageActions.ts) to a copy of the garage saved on the device, and keeps it only when the action went through, as the server does. An ad reward is paid without a server confirming it. There is no anti-cheat, so a local build has no leaderboards and its save never reaches a server account. -
Local race judge (
race/judge/localJudge.ts, without the server): career races, boss fights, tournaments, wagers, quick and weekly races are started, replayed and paid on the device by the same functions the server calls (openTicket.ts,settleRace.ts,seriesRound.tsinrace/judge/). What the server keeps in its database lives in memory (localTickets.ts): the tickets and the series in progress, so a reload forfeits a running series, as the game already does. The double prize and Keep rounds pay on the ad’s own callback. No leaderboard times.
ESLint enforces the split (SERVER_CALLERS in eslint.config.mjs): outside player/account/,
net/ and boot/, importing authedCall or API_BASE, or using fetch or WebSocket, is an error.
A new feature that needs the server adds its call in player/account/ and, if it changes the garage,
a method on the backend.
The game reaches ads through core/ads/provider.ts only (interface in core/ads/types.ts): start
with consent, rewarded ready and show, the privacy form, and pausing while a full-screen ad shows
(boot/adPause.ts). admob is core/ads/admobProvider.ts over the AdMob files; none is
core/ads/noAds.ts, which shows no rewarded buttons except with ?fakeads in dev. A portal’s or
YouTube’s SDK becomes another provider.
Features that need the server
Section titled “Features that need the server”Without online, a build leaves out:
- the online race, leaderboard, friends and inbox scenes (
boot/onlineScenes.ts, so their code is not in the bundle); - Online and Leaderboards in the main menu’s left column (the rest moves up);
- the inbox in the top bar, the nickname lookup in the racer badge, the friend request badge;
- the account buttons in Settings (link, delete, my data) and the friend tunes in Tuning.
Without purchases, canBuy() (player/account/purchases.ts) is false and the gold packs do not
show. Quick race and the weekly event stay: the local judge pays them.