State Management
Store responsibilities
The central client state store (Zustand + Immer) is responsible for:
- Holding the complete board state that the UI renders (board, notes, Jira items)
- Tracking UI state (selected note, open overlays, active editor, search state)
- Dispatching user actions that:
- Update local state immediately (optimistic render)
- Persist to IndexedDB (offline durability)
- Queue a sync push to D1 (server persistence)
- Optionally queue a changelog write to R2
Optimistic update pattern
All mutations follow this sequence:
User action
→ Update Zustand store immediately (instant render)
→ Save to IndexedDB (offline durability)
→ schedulePush() to D1SyncManager (server persistence)
→ optionally queue ChangeLogger entry (audit trail)Why this matters: The UI never waits for a network round-trip. The user's action takes effect instantly. If the push fails, a retry happens automatically.
Example: creating a note
createNote: (note) => {
// 1. Optimistic local update — instant
set((s) => { s.notes[note.id] = note })
// 2. Push to server — asynchronous
if (syncManager.ready) {
syncManager.createNote(note)
}
},Store shape overview
| State slice | What it holds |
|---|---|
board | Board metadata (name, dimensions, ownerEmail) |
notes | Record of all notes by ID |
jiraItems | Record of all Jira work items by ID |
selectedNoteId | Currently focused note |
overlayNoteId | Note open in the full-screen editor overlay |
activeJiraItemId | Jira item open in the editor |
isAiPanelOpen | Whether the AI chat panel is visible |
phases / currentPhaseId | PM lifecycle phase config |
searchQuery / searchMatchIds | Full-text search state |
canEdit | Whether the current user can make edits |
undoStack | Last 20 deleted notes (for Ctrl+Z restore) |
subagent | Floating subagent panel position and state |
connection | Sync connection status |
Selector discipline
Use Zustand selectors to subscribe only to the slices you need:
// ✓ Subscribe to a specific slice — re-renders only when notes change
const notes = useBoardStore((s) => s.notes)
// ✗ Subscribe to entire store — re-renders on any state change
const store = useBoardStore()Write permissions
The canEdit flag is set from the board access check response. All write operations in components check canEdit before proceeding. The Bottom Toolbar, canvas double-click (create note), and the note editor are all gated on this flag.
Undo delete
Deleting a note snapshots it into undoStack (max 20 entries). Ctrl / Cmd + Z on the canvas triggers undo(), which re-creates the note in local state and pushes it to D1.
See also
- Sync Engine — How changes get to the server
- Storage Layers — IDB and D1 roles