Toast
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 through apiFetch only.
  • POL-003: All subscriptions must be selector-based (useShallow for object/array composites).
  • POL-004: User-affine persisted keys must be scoped by siteId + 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-level store.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:

  1. URL/search/navigation state: keep in TanStack Router (POL-001).
  2. Canonical route payloads: load in route loaders (POL-001).
  3. Shared workflow state across components/routes: use Zustand stores and selectors.
  4. 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 useShallow for object/array selections.

Required Guardrails

  • No direct backend fetch in admin stores or view-model hooks. Use apiFetch (POL-002).
  • No full-store subscriptions in UI/view-model code; always pass selectors (POL-003).
  • No direct feature-level store.setState calls 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(...).

Context Exceptions

A new app-level shared feature context requires:

  1. Why store class A/B is insufficient.
  2. Maintainer approval in PR review.
  3. Durable documentation note in this folder.

Enforcement Matrix

PolicyGuardrailTool
POL-002Disallow raw fetch in stores/view-model hooksESLint guards
POL-003Disallow full-store subscriptionsESLint guards
POL-004Validate persistence key scopeStore contract tests
POL-005Block persistence of private settings/secretsStore contract tests + PR checklist
POL-006Disallow direct feature-level store.setStateESLint guards
POL-007Scoped-store lifecycle invariantsScoped provider tests
POL-008Context exception approval + durable notePR checklist + maintainer review
POL-009100% coverage and reset disciplinepnpm check:strict

On this page