PURPOSE

Single entry point invoked once on every app open, after the auth session is established and before the app shell renders. Calls the bootstrap_player Supabase RPC, which atomically ensures all player rows (profile, wallet, inventory, save) exist, runs save migrations on the returned blob, and hydrates every Zustand store that depends on canonical server state. Fast-fails with SUPABASE_TIMEOUT: bootstrap unreachable after a hard ceiling so the splash can offer offline mode instead of freezing.

OWNS

  • bootstrapPlayer() — the single public async entry point. Wraps _doBootstrap() in a Promise.race against a 5-second timeout.
  • _doBootstrap() — internal sequence: RPC call → save migration → store hydration → secondary get_mod_grid RPC → return payload.
  • BootstrapPayload — exported TypeScript interface describing the shape of the bootstrap_player RPC response (profile, wallet, tickets, ships map, mod grid, legacy v3 inventory rows, pity, save blob, tier records/claims, challenge completions, planet stats, planet XP, planet reward claims).
  • BOOTSTRAP_TIMEOUT_MS — hard ceiling constant for the timeout race.
  • collapseToHull() — local helper that normalizes legacy ${hull}_${rarity} template ids (or bare hull ids) down to a bare hull_class string.

READS FROM

  • bootstrap_player Supabase RPC via invokeRpc from ./supabase.
  • get_mod_grid Supabase RPC via invokeRpc (secondary call after bootstrap; migration 040 dependency).
  • HULL_CLASSES from @starship-survivors/data/ships — used to filter hulls in the payload against the canonical hull whitelist.
  • runSaveMigrations from @starship-survivors/data/save-migrations — applied to payload.save before any downstream consumer touches it.
  • Type imports: SaveBlob, ShipInstance, ModGridSnapshot, TierRecordRow, TierClaimRow, ChallengeCompletionRow, PlanetStatsRow, PlanetXpRow, PlanetRewardClaimRow, WalletSnapshot, TicketSnapshot.

PUSHES TO

Hydrates the following Zustand stores from the server snapshot:

  • useWalletStore.replaceFromSnapshot(wallet, tickets).
  • useInventoryStore.loadInventory(shipsMap) when at least one hull resolved, otherwise resetToStarter().
  • useSessionStore.hydrateFromSave(collapseToHull(payload.save.selected_ship_id)).
  • useModGridStore.loadFromSnapshot(modGrid) when get_mod_grid returned a non-empty object, otherwise resetToStarter() (also taken on RPC error).
  • useTierStore.loadFromBootstrap(tier_records, tier_claims) — empty-array fallbacks when fields absent.
  • useChallengeStore.loadFromBootstrap(challenge_completions, planet_stats) — empty-array fallbacks when fields absent.
  • usePlanetProgressStore.loadFromBootstrap(planet_xp, planet_reward_claims) — empty-array fallbacks when fields absent.

Returns the (migrated) BootstrapPayload to the caller so consumers such as playerStore can read directly from it.

DOES NOT

  • Does not establish or refresh the auth session — must be called after auth is already in place.
  • Does not render or mount any UI. Renders are gated externally on the returned promise.
  • Does not persist or write back to Supabase — read-only bootstrap, no set_* or claim_* RPCs invoked.
  • Does not retry on failure. A single failed attempt rejects; the splash is responsible for offering retry or offline mode.
  • Does not handle the get_mod_grid RPC error as fatal — it is caught and logged via console.warn, then the store falls back to its starter state. The primary bootstrap_player RPC failure is not caught and propagates to the caller.
  • Does not write to local storage or any other persistence layer.
  • Does not bootstrap captains via the ships-map path — the v3 fallback explicitly filters entity_type === 'ship'; captains remain on the captain path.
  • Does not include hulls that are absent from HULL_CLASSES, even if the server returns them.

Signals

  • Rejection: Error('SUPABASE_TIMEOUT: bootstrap unreachable') when the RPC does not resolve within BOOTSTRAP_TIMEOUT_MS (5000 ms).
  • Rejection: any error thrown by invokeRpc('bootstrap_player') propagates unchanged to the caller.
  • Warning: console.warn('[playerBootstrap] get_mod_grid unavailable — using starter mod grid:', err) on get_mod_grid RPC failure (non-fatal).
  • Resolution: returns the BootstrapPayload with save replaced by the migrated SaveBlob.

Entry points

  • bootstrapPlayer(): Promise<BootstrapPayload> — exported async function. The sole public surface.
  • BootstrapPayload interface — exported for callers that need to type the returned payload.

Pattern notes

  • Hard-ceiling timeout mirrors the auth-flow pattern: a dead Supabase instance must surface within ~5 s so the splash can offer offline mode rather than freezing on a loading screen.
  • Inventory hydration follows a v4-then-v3 ladder: payload.ships (hull_class → { xp } map) is preferred; when absent or empty, the legacy payload.inventory row array is filtered to ships, ids are collapsed via collapseToHull, and duplicate rows are counted into XP. When neither path yields a hull, resetToStarter() is invoked.
  • Mod grid is a deliberate exception to the atomic-bootstrap pattern: bootstrap_player does not yet return mod_grid_json, so get_mod_grid runs as a separate awaited RPC inside _doBootstrap and is wrapped in try/catch so an un-migrated server falls back to starter state instead of failing the whole bootstrap.
  • All “may be absent” payload fields (tier_records, tier_claims, challenge_completions, planet_stats, planet_xp, planet_reward_claims) are passed through Array.isArray(...) ? field : [] before being handed to store loaders, so missing-migration servers degrade gracefully rather than crashing the hydrate step.
  • Save migration runs synchronously on the returned blob before any store hydrates — useSessionStore.hydrateFromSave therefore always sees a current-schema save.
  • collapseToHull is applied in two places: when consuming legacy template_id strings from the v3 inventory fallback, and when reading payload.save.selected_ship_id so suffix-form persisted selections still resolve to a bare hull.
  • The function is intentionally non-defensive at the outer boundary: a failed bootstrap_player RPC is treated as a hard failure that the caller surfaces, matching the “crash on bad data, no silent fallbacks” rule for canonical state.