ContributorPatterns
Admin State Playbook
Admin State Playbook
Implementation playbook for shared state work in apps/admin.
Canonical Policies
POL-001: TanStack Router owns canonical route data loading; stores orchestrate shared workflow state after load.POL-002: Stores and view-model hooks call Toast backend APIs throughapiFetchonly.POL-003: All subscriptions must be selector-based (useShallowfor object/array composites).POL-004: User-affine persisted keys must be scoped bysiteId + userId.POL-005: Private settings, secrets, auth artifacts, and sensitive config are never persisted client-side.POL-006: Feature code mutates through typed actions/view-model commands; no direct feature-levelstore.setState.POL-007: Scoped store instances are created once per provider lifecycle (useState/useRef) and recreated only on scope key change.POL-008: New app-level shared contexts require documented exception rationale and maintainer approval.POL-009: Touched store/view-model files must keep 100% coverage and deterministic reset semantics.POL-010: Guardrails are enforced in CI via lint/test/docs checklist.
Scope Decision
Use this order before adding state:
- URL/search/navigation state: keep in TanStack Router (
POL-001). - Canonical route payloads: load in route loaders (
POL-001). - Shared workflow state across components/routes: use Zustand stores and selectors.
- Local one-component UI state: keep local (
useState).
Store Topology
- Route workflow domains live in
apps/admin/src/stores/*.store.ts(content list, settings, audit, API status). - Editor runtime concerns live in scoped stores under
apps/admin/src/stores/scoped/**. - Loader-first flow is required for route-entry canonical payloads; hooks should skip redundant mount-fetches when loader-preloaded state is already settled.
UI Boundary
- UI surfaces (
routes/,components/) consume view-model hooks (use*VM) instead of importing route-store internals directly. - View-model hooks are the adapter between UI and store actions/selectors.
- Keep selectors narrow and use
useShallowfor object/array selections.
Required Guardrails
- No direct backend
fetchin admin stores or view-model hooks. UseapiFetch(POL-002). - No full-store subscriptions in UI/view-model code; always pass selectors (
POL-003). - No direct feature-level
store.setStatecalls outside test/reset/bootstrap paths (POL-006). - Global route stores (
apps/admin/src/stores/*.store.ts) stay bundle-light. Heavy editor/collab deps belong in scoped stores. - Private settings/secrets are never persisted client-side (
POL-005).
Persistence Rules
- User-affine keys must use:
toast-admin:{siteId}:{userId}:{storeName}:v{N}(POL-004). - Site-shared keys are allowed only with explicit documented justification.
- On logout, tenant switch, or user switch:
- Reset in-memory route stores via
resetAdminRouteStores(). - Purge persisted user-affine editor session keys via
purgeEditorSessionUserStorage(...).
- Reset in-memory route stores via
Context Exceptions
A new app-level shared feature context requires:
- Why store class A/B is insufficient.
- Maintainer approval in PR review.
- Durable documentation note in this folder.
Enforcement Matrix
| Policy | Guardrail | Tool |
|---|---|---|
POL-002 | Disallow raw fetch in stores/view-model hooks | ESLint guards |
POL-003 | Disallow full-store subscriptions | ESLint guards |
POL-004 | Validate persistence key scope | Store contract tests |
POL-005 | Block persistence of private settings/secrets | Store contract tests + PR checklist |
POL-006 | Disallow direct feature-level store.setState | ESLint guards |
POL-007 | Scoped-store lifecycle invariants | Scoped provider tests |
POL-008 | Context exception approval + durable note | PR checklist + maintainer review |
POL-009 | 100% coverage and reset discipline | pnpm check:strict |