apps/web/src/store/. Every store uses the Immer middleware so reducers read like mutation. IndexedDB is the persistence layer (MagicResumeDB via src/lib/api/IndexDBClient.ts).
Stores
The big store: useResumeStore
This holds the active resume plus a syncStatus machine:
- Update the in-memory state (Immer).
- Persist to IndexedDB synchronously-after-render.
- If cloud mode and
useSettingStore.cloudSyncis on, schedule a debounced push to the Core API.
The cloud sync side-effect runs only when both
APP_MODE === 'cloud' and the user opted in via Settings. Self-hosted users never trigger a network request from the store.IndexedDB layer
src/lib/api/IndexDBClient.ts is a thin wrapper around idb-keyval. Keys are namespaced per store (e.g. resume:current, resume:history:<id>).
When you add a new persisted field, add a versioned migration if the shape changes. Silent shape drift will surface as runtime errors weeks later when an old browser tab opens.
Why Zustand?
Three reasons:- No provider boilerplate. Stores are imported directly; no
<Provider>chain. - Selector subscriptions. Components subscribe to slices, not the whole store, so editing one field doesn’t re-render the whole tree.
- Easy to bridge to non-React. The MCP package, e2e helpers, and AI flows reuse the same
Resumetype from@magic-resume/resume-schemawithout dragging React along.
