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 Supabaseplayers.display_name, mirrors to store.setAnalyticsOptIn(optIn)— updates Supabaseplayers.analytics_opt_in, mirrors to store.isAdmin()— read-only role check against the locally-cached profile.getAccountState()— returns'guest' | 'claimed' | nullfrom the local profile.getDisplayName()— returns the local display name, falling back to'Pilot'.
READS FROM
usePlayerStore.getState().profile— source of truth forid,role,accountState,displayName.- Supabase
playerstable via thesupabaseclient (./supabase).
PUSHES TO
- Supabase
playerstable (UPDATE … WHERE id = profile.id) fordisplay_nameandanalytics_opt_in. usePlayerStore—setStatepatchesprofilewith the newdisplayName/analyticsOptInafter the Supabase write succeeds.
DOES NOT
- Bootstrap or hydrate the profile — that happens via the
bootstrap_playerRPC on the read-side. - Promote a player to admin — role changes happen via SQL or an
admin_grantRPC, never through this module. - Create or delete
playersrows. - 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'whenplayerStore.profileis 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
playerStoresubscriptions.
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.