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 aPromise.raceagainst a 5-second timeout._doBootstrap()— internal sequence: RPC call → save migration → store hydration → secondaryget_mod_gridRPC → return payload.BootstrapPayload— exported TypeScript interface describing the shape of thebootstrap_playerRPC 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 barehull_classstring.
READS FROM
bootstrap_playerSupabase RPC viainvokeRpcfrom./supabase.get_mod_gridSupabase RPC viainvokeRpc(secondary call after bootstrap; migration 040 dependency).HULL_CLASSESfrom@starship-survivors/data/ships— used to filter hulls in the payload against the canonical hull whitelist.runSaveMigrationsfrom@starship-survivors/data/save-migrations— applied topayload.savebefore 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, otherwiseresetToStarter().useSessionStore.hydrateFromSave(collapseToHull(payload.save.selected_ship_id)).useModGridStore.loadFromSnapshot(modGrid)whenget_mod_gridreturned a non-empty object, otherwiseresetToStarter()(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_*orclaim_*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_gridRPC error as fatal — it is caught and logged viaconsole.warn, then the store falls back to its starter state. The primarybootstrap_playerRPC 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 withinBOOTSTRAP_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)onget_mod_gridRPC failure (non-fatal). - Resolution: returns the
BootstrapPayloadwithsavereplaced by the migratedSaveBlob.
Entry points
bootstrapPlayer(): Promise<BootstrapPayload>— exported async function. The sole public surface.BootstrapPayloadinterface — 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 legacypayload.inventoryrow array is filtered to ships, ids are collapsed viacollapseToHull, 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_playerdoes not yet returnmod_grid_json, soget_mod_gridruns as a separate awaited RPC inside_doBootstrapand is wrapped intry/catchso 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 throughArray.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.hydrateFromSavetherefore always sees a current-schema save. collapseToHullis applied in two places: when consuming legacytemplate_idstrings from the v3 inventory fallback, and when readingpayload.save.selected_ship_idso suffix-form persisted selections still resolve to a bare hull.- The function is intentionally non-defensive at the outer boundary: a failed
bootstrap_playerRPC is treated as a hard failure that the caller surfaces, matching the “crash on bad data, no silent fallbacks” rule for canonical state.