PURPOSE

Manages player profile mutations against Supabase. Handles display name edits and analytics opt-in toggling, plus read-only helpers for role, account state, and display name. Writes confirm against Supabase before mirroring into the local playerStore.

OWNS

  • updateDisplayName(newName) — trims, validates length (1–24 chars), updates Supabase players.display_name, mirrors to store.
  • setAnalyticsOptIn(optIn) — updates Supabase players.analytics_opt_in, mirrors to store.
  • isAdmin() — read-only role check against the locally-cached profile.
  • getAccountState() — returns 'guest' | 'claimed' | null from the local profile.
  • getDisplayName() — returns the local display name, falling back to 'Pilot'.

READS FROM

  • usePlayerStore.getState().profile — source of truth for id, role, accountState, displayName.
  • Supabase players table via the supabase client (./supabase).

PUSHES TO

  • Supabase players table (UPDATE … WHERE id = profile.id) for display_name and analytics_opt_in.
  • usePlayerStoresetState patches profile with the new displayName / analyticsOptIn after the Supabase write succeeds.

DOES NOT

  • Bootstrap or hydrate the profile — that happens via the bootstrap_player RPC on the read-side.
  • Promote a player to admin — role changes happen via SQL or an admin_grant RPC, never through this module.
  • Create or delete players rows.
  • Cache or retry Supabase writes; errors propagate as thrown Errors.
  • Update the store optimistically — store mirrors only after Supabase confirms.

Signals

  • Throws 'No profile loaded — bootstrap incomplete' when playerStore.profile is null on mutation entry.
  • Throws 'Display name must be 1-24 characters' when the trimmed name is out of range.
  • Throws Failed to update display name: <supabase error> / Failed to update analytics opt-in: <supabase error> when the Supabase write returns an error.
  • No event-bus emissions; downstream UI observes changes through playerStore subscriptions.

Entry points

  • updateDisplayName — called from profile/settings screens when the user submits a new display name.
  • setAnalyticsOptIn — called from privacy/settings UI when the analytics toggle changes.
  • isAdmin — gated UI affordances (admin panels, debug tools) read this synchronously.
  • getAccountState — used to branch UI between guest and claimed flows.
  • getDisplayName — read by leaderboard rows, greeting strings, and any UI that needs the current name without subscribing to the store.

Pattern notes

  • Confirm-then-mirror: Supabase write must succeed before the local store is patched. Failures throw and leave the store untouched.
  • The mutation functions read the profile at call time via getState() rather than capturing a reference; safe for stale-closure concerns but assumes the profile cannot change mid-await.
  • Validation lives in the service, not the UI — display-name length and trimming are enforced here so any caller gets the same contract.
  • Read-only helpers are synchronous and non-throwing; they return sensible defaults (null, 'Pilot') when the profile is missing rather than asserting bootstrap state.
  • Role promotion is intentionally out-of-band: clients can only read role, never write it.