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 in code/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 RewardBatch commit-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-stats round-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.
  • invokeRpc and the shared supabase client from @metagame/services/supabase — the single chokepoint for every server-authoritative call in the layer.
  • The bridge-side MissionResult shape produced at onGameOver, fed verbatim to runProgressionService.finalizeRun.

PUSHES TO

  • Supabase RPCs invoked from the layer: perform_pull (pullService), finalize_run (runProgressionService), grant_fake_gems (shopService), dev_unlock_everything and dev_reset_account (unlockService), bootstrap_player (unlockService re-hydrate path, via bootstrapPlayer()).
  • 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) and useRewardQueueStore.enqueue (after describe), one batch per reward source.
  • POST /__dev/push-stats for the dev Ship Playground push-button flow; Sentry breadcrumbs / messages / exceptions for every push-failure category.
  • @sentry/browser breadcrumbs on RPC failure paths (wrapped inside invokeRpc) and explicit captureException / captureMessage in playgroundPush.

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 in pullService. 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

FilePurpose
assembleRunServiceSingle bridge function assembleRunDef — composes a frozen RunDefinition from ship/planet/mission/challenge selections; every entry into a run goes through this.
runProgressionServicefinalizeRun(result, planetIndex) — calls the finalize_run Supabase RPC after onGameOver, reconciles wallet/tier/challenge/planet stores from the canonical response.
pullServiceV4 banner pull: client rolls hull + rarity, perform_pull RPC validates and persists, local inventory advances via grantShipPull.
mission-dropsPost-mission Mod Grid drop service — picks drop count from tier vs. PB, rolls templates and rarities, commits to inventory, marks unseen.
modGridServicePure mod-grid math — canPlace, computeOccupancy, computeEquippedStatBonus, statsForInstance. No store reads, no side effects.
shopServiceBeta shop: gem-pack top-up via grant_fake_gems, plus transitional local flows for special offers, Supporter Club, and daily ad-ticket claims.
unlockServiceDev-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-finalizersCommit-layer for the reward pipeline — finalizeRunRewards / finalizeMissionRewards / finalizePullRewards / finalizeChestRewards. Each finalizer is the single entry point for its reward source.
reward-describersPure translators from raw reward events to structured RewardBatch payloads. Currently scaffolded — describers return empty batches with deterministic IDs.
reward-presentersStrategy 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-registryStub useAnchor(anchorId) React hook for naming DOM anchor points (fly-to targets, counter positions). Currently returns an unmanaged ref.
playgroundPushShared 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 — where assembleRunDef is consumed and finalizeRun is called.
  • code/data/reward-types — the RewardBatch shape the finalizer / describer / presenter trio operates on.

12 items under this folder.