services
PURPOSE
The services layer — stateless modules and store-coordinating orchestrators that compose stores, data tables, and Supabase RPCs into the operations the rest of the app calls. Each file is the single chokepoint for one cross-cutting concern (assemble a run, finalize a match, perform a pull, drop loot, validate mod placement, present a reward, top up the shop, dev-reset an account, push dev stats). The layer splits into two halves:
- Gameplay services at
src/starship-survivors/services/— game-side concerns wired into the engine bridge, the run pipeline, and the reward pipeline. The 12 files documented below. - Metagame services at
src/metagame/services/— Supabase client, auth lifecycle, cold-start bootstrap, profile mutations, feature flags, analytics batching. Covered separately incode/metagame/services.
Service modules either own only pure functions (modGridService, reward-presenters, mission-drops math, pullService rolls) or own an orchestration sequence that snapshots stores, calls an RPC, and reconciles canonical server state back into local stores (assembleRunService, runProgressionService, pullService.executePullPull, shopService, unlockService, reward-finalizers). No service renders UI, owns a route, or schedules a frame.
OWNS
- The single entry point per cross-cutting concern. Callers never reach into the underlying RPC or store directly when a service exists for the operation.
- The composition order between stores, data tables, RPCs, and analytics for every operation in the layer (snapshot → lock → RPC → reconcile → emit).
- The
RewardBatchcommit-then-describe contract:reward-finalizers(commit + enqueue) →reward-describers(describe) →reward-presenters(reveal/collect strategy). - The dev-only account-reset and account-max utilities — there is no other path that grants 5★ ships or wipes an account.
- The mod-grid placement validation primitives consumed by both the UI and any future server-side validator.
- The post-mission drop-count curve and drop-roll randomization.
- The v4 pull rolling protocol — client rolls hull + rarity, server validates and persists, client reconciles wallet/pity/inventory.
- The dev-only
/__dev/push-statsround-trip used by Ship Playground PUSH buttons, with Sentry-tagged failure categories.
READS FROM
- Every Zustand store the layer needs to snapshot or reconcile:
useWalletStore,useInventoryStore,usePlayerStore,useSessionStore,useModGridStore,useTierStore,useChallengeStore,usePlanetProgressStore,useSupporterStore,useRewardQueueStore,useDisplayOverrideStore. - Data tables for stats, curves, and templates:
data/ships,data/mods,data/mod-templates,data/mods/grid-unlock,data/economy(GEM_PACKS,SPECIAL_OFFERS,computeArcadeCredits),data/reward-types,data/save-migrations. invokeRpcand the sharedsupabaseclient from@metagame/services/supabase— the single chokepoint for every server-authoritative call in the layer.- The bridge-side
MissionResultshape produced atonGameOver, fed verbatim torunProgressionService.finalizeRun.
PUSHES TO
- Supabase RPCs invoked from the layer:
perform_pull(pullService),finalize_run(runProgressionService),grant_fake_gems(shopService),dev_unlock_everythinganddev_reset_account(unlockService),bootstrap_player(unlockService re-hydrate path, viabootstrapPlayer()). - Zustand stores as the reconciliation target of every RPC return: wallet replace-from-snapshot, inventory grants, pity map, tier/challenge/planet-progress loads, mod-grid drops, supporter-club state, player sync status.
- Analytics event-name constants from
@metagame/services/analytics— pull unlocks and XP gains, mod-template unlocks, mod-buy-place, mod-destroy. - The reward pipeline:
useDisplayOverrideStore.lockForBatch(before commit) anduseRewardQueueStore.enqueue(after describe), one batch per reward source. POST /__dev/push-statsfor the dev Ship Playground push-button flow; Sentry breadcrumbs / messages / exceptions for every push-failure category.@sentry/browserbreadcrumbs on RPC failure paths (wrapped insideinvokeRpc) and explicitcaptureException/captureMessageinplaygroundPush.
DOES NOT
- Render any UI, mount React, or own a route. The layer is consumed by screens and components but never reaches into them.
- Own its own state. Module-level mutables are limited to a one-time-offer set (
shopService.purchasedOffers), a batch-id counter (reward-describers._batchCounter), and the in-flight transitional fields callers see inpullService. Canonical state always lives in stores or on the server. - Schedule frames, tick, or subscribe to the engine. Services are called from React event handlers, the bridge, or other services — never from inside the gameplay loop.
- Decide when an operation runs. The splash decides when to bootstrap, the bridge decides when to finalize a run, the UI decides when to open the shop. Services only expose the operation.
- Hide RPC errors. Every async service surfaces failures to the caller via thrown errors,
PurchaseResult.error,PushResult.reason, or rejected promises. Sync-status transitions ('syncing' → 'synced' | 'error') are bracketed around every RPC call. - Maintain offline queues. There is no on-disk replay buffer for failed RPCs in this layer; failures are surfaced to the caller and the user retries.
Files
| File | Purpose |
|---|---|
assembleRunService | Single bridge function assembleRunDef — composes a frozen RunDefinition from ship/planet/mission/challenge selections; every entry into a run goes through this. |
runProgressionService | finalizeRun(result, planetIndex) — calls the finalize_run Supabase RPC after onGameOver, reconciles wallet/tier/challenge/planet stores from the canonical response. |
pullService | V4 banner pull: client rolls hull + rarity, perform_pull RPC validates and persists, local inventory advances via grantShipPull. |
mission-drops | Post-mission Mod Grid drop service — picks drop count from tier vs. PB, rolls templates and rarities, commits to inventory, marks unseen. |
modGridService | Pure mod-grid math — canPlace, computeOccupancy, computeEquippedStatBonus, statsForInstance. No store reads, no side effects. |
shopService | Beta shop: gem-pack top-up via grant_fake_gems, plus transitional local flows for special offers, Supporter Club, and daily ad-ticket claims. |
unlockService | Dev-only account utility — dev_unlock_everything (5★ everything, max wallet, all mods Legendary, all grid cells) and dev_reset_account, both followed by bootstrapPlayer() re-hydrate. |
reward-finalizers | Commit-layer for the reward pipeline — finalizeRunRewards / finalizeMissionRewards / finalizePullRewards / finalizeChestRewards. Each finalizer is the single entry point for its reward source. |
reward-describers | Pure translators from raw reward events to structured RewardBatch payloads. Currently scaffolded — describers return empty batches with deterministic IDs. |
reward-presenters | Strategy pattern for the reward pipeline’s reveal/collect phases. Built-in defaultPresenter (card-flip + fly-to-target) and pullPresenter (pass-through to pull engine). |
anchor-registry | Stub useAnchor(anchorId) React hook for naming DOM anchor points (fly-to targets, counter positions). Currently returns an unmanaged ref. |
playgroundPush | Shared client helper for Ship Playground PUSH buttons — POST /__dev/push-stats, routes every failure mode to Sentry with full pivot tags. Dev-only endpoint. |
See also
code/metagame/services— Supabase-side services (client +invokeRpc, auth, bootstrap, profile, feature flags, analytics).code/stores— every store the layer reconciles into.code/engine/bridge— whereassembleRunDefis consumed andfinalizeRunis called.code/data/reward-types— theRewardBatchshape the finalizer / describer / presenter trio operates on.