Adopt Per-Operator Preferences from rwe-core
- Status: proposed
- Deciders: Platform Team
- Date: 2026-06-29
- Parent: 0001 — Adopt WhiskerPlatform over rwe-core
- Extended by: 0009 — Enforced store/global config vs. per-operator preference (splits cosmetic preference, kept here, from ops-required monitoring config)
Technical Story: Athlete-scoped preferences vs device-scoped localStorage
Context and Problem Statement
Section titled “Context and Problem Statement”Store athletes share devices, switch stores, and expect their dismissals, layouts, and flags to follow them — not the browser profile on a shared iPad.
rwe-core keys several dashboard behaviours by operator_id
(from useAuth().userInfo):
- Widget dismiss persistence:
localStorage.widgetsLastHiddenByUser[operatorId][widgetId] - Per-athlete feature flags via
deledge-based overrides (host-level)
whisker-host today persists much user-visible state per device in localStorage:
- Dashboard layout:
dashboard-layout-store(Zustand persist — not scoped byoperator_id) - Sidebar, theme, language — device-local patterns documented in Risks & improvements — end users
On a shared store device, Athlete A’s dashboard layout or widget dismissals may appear for Athlete B after login switch. rwe-core’s widget dismiss model avoids that for widgets; Whisker does not yet.
This ADR decides what to adopt from rwe-core — not whether to abandon Whisker, but which user-scoping model Whisker must implement before rwe-core migration claims parity.
Related: 0006 — Widget host model (widget dismiss is one instance of this problem).
Decision Drivers
Section titled “Decision Drivers”- Shared devices in stores — Same hardware, different athletes per shift
- Athlete continuity — Dismissals and layout should not leak across users
- Parity with rwe-core migration — Workflows that relied on per-athlete flags must not silently break
- Storage simplicity — localStorage is easy but wrong scope without keying strategy
- Future BFF — Server-side preferences may belong on
whisker-bfflong term
Considered Options
Section titled “Considered Options”Option A — Keep device-scoped localStorage (Whisker today)
Section titled “Option A — Keep device-scoped localStorage (Whisker today)”No change; accept that preferences are per-browser.
Option B — Adopt rwe-core’s operator-keyed localStorage pattern
Section titled “Option B — Adopt rwe-core’s operator-keyed localStorage pattern”Namespace all preference keys by operator_id from useAuth().userInfo
(client-side only, same storage medium).
Option C — Server-side preferences via BFF
Section titled “Option C — Server-side preferences via BFF”Persist per-operator layout, dismissals, flags in a store API; localStorage as cache only.
Option D — Hybrid
Section titled “Option D — Hybrid”Operator-keyed localStorage short term; BFF-backed preferences when
whisker-bff lands.
Pros and Cons
Section titled “Pros and Cons”Option A — Device-scoped
Section titled “Option A — Device-scoped”- Good: simplest implementation; no backend.
- Bad: wrong UX on shared devices; migration blocker from rwe-core; diverges from athlete mental model.
Option B — Operator-keyed localStorage
Section titled “Option B — Operator-keyed localStorage”- Good: matches rwe-core widget dismiss pattern; no backend required for first slice; quick to ship.
- Bad: still client-only — cleared if storage wiped; no cross-device sync for same athlete on another iPad.
Option C — Server-side
Section titled “Option C — Server-side”- Good: true per-athlete profile; cross-device; audit trail possible.
- Bad: requires BFF/API design, auth, storage, migration — not ready for immediate pilot.
Option D — Hybrid (recommended path)
Section titled “Option D — Hybrid (recommended path)”- Good: closes shared-device gap quickly; leaves room for server profile; aligns with rwe-core interim behaviour.
- Bad: two-phase implementation; teams must migrate keys once BFF ships.
Decision Outcome
Section titled “Decision Outcome”Chosen option: Option D — hybrid.
Near term (adopt from rwe-core):
- Key dashboard layout, widget dismiss state, and similar preferences
by
operator_id— mirror rwe-core’swidgetsLastHiddenByUser[operatorId][widgetId]pattern. - Clear or swap preference namespaces on login/logout when
operator_idchanges.
Medium term:
- Design server-backed athlete preferences on
whisker-bff(or flags service) — resolves risk #10 (per-athlete feature overrides / deledge parity).
Explicitly not deciding here: exact API shape for server preferences;
manifest userOverrides vs separate flags service.
Option A is rejected as steady state for store shared devices.
Positive Consequences
Section titled “Positive Consequences”- Shared iPads behave correctly when athletes hand off.
- Migration from rwe-core can cite explicit preference parity plan.
- Reuses proven rwe-core keying without copying entire host.
Negative Consequences
Section titled “Negative Consequences”- Whisker must audit all persist stores for operator scoping — not only dashboard layout.
- Client-only prefs still do not sync across devices for one athlete until server phase ships.
- deledge / feature-flag parity remains a separate workstream (risk #10).
Recommendations
Section titled “Recommendations”- Inventory every
localStorage/sessionStoragepersist inwhisker-hostand WCL providers; addoperator_idnamespace where user-specific. - Implement widget dismiss persistence keyed by operator when 0006 lifecycle work lands.
- Block rwe-core migration sign-off for modules that depend on per-athlete flags until risk #10 has a decided approach.
- rwe-core:
WidgetHostProvider.tsx—widgetsLastHiddenByUser - whisker-host:
src/store/dashboardLayoutStore.ts—dashboard-layout-store - Risk #10 — per-athlete overrides
- Comparison — per-operator dismiss
