Skip to content

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.

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).

Everything that changes the garage or the career goes through a backend, in two halves:

  • player/backend/backend.ts (interface in player/backend/types.ts): load the garage, a shop’s GarageAction and ad rewards.

  • race/judge/raceBackend.ts (interface in race/judge/types.ts): career, quick and weekly race tickets and their judging, the double prize and keeping boss rounds. It sits in the race module because judging replays the race (race/replayEventRace.ts), which player may not import.

  • Remote (player/backend/remote.ts, when online): the race server owns the garage, as Architecture describes. The calls live in player/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.ts in race/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.

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.