Store Architecture
Writan uses Zustand for state management, with a local-first architecture backed by IndexedDB and synced to Supabase.
Stores
projectStore — Data State
Located at lib/stores/projectStore.ts. Manages all project data and operations:
- Data: projects, nodes, pips, tree hierarchy
- Project CRUD: create, rename, delete projects
- Node operations: add, rename, delete, reorder nodes; update prose, data blocks, generation layer
- Pip management: create craft pips, update pip status with dual-write (pips table + node.generation.decisions)
- Undo system: 30-level undo stack using node/pip snapshots
uiStore — UI State
Located at lib/stores/uiStore.ts. Manages transient UI state:
- Sidebar collapse states and widths
- Selected node, expanded nodes
- Active pip, review filters
- Output column visibility and follow mode
- Focus mode, settings modal, left sidebar mode
ruleConfigStore — Rule Configuration
Located at lib/stores/ruleConfigStore.ts. Tracks which writing rules are enabled/disabled:
- Persists to localStorage
- Can disable by authority prefix (e.g. all Orwell rules) or individual rule ID
- Used by
analyzeNode()to filter which detectors run
Data Flow Pattern
Writan follows a local-first pattern:
User action
→ Write to IndexedDB (Dexie)
→ Queue sync operation
→ Trigger sync (batches to Supabase every 3 seconds, max 50 entries)
→ Update Zustand state
→ Rebuild the tree viewSync Engine (lib/sync/syncEngine.ts)
- Coalesces operations (insert + delete = cancel, multiple updates = last wins)
- Batches up to 50 entries per sync cycle
- Supports offline-first — queued operations persist in IndexedDB
- Checks
navigator.onLinebefore attempting sync
Local Database (lib/db/localDb.ts)
IndexedDB via Dexie stores: projects, nodes, pips, user settings, and the sync queue.
Component Patterns
- All components are
'use client'— no React Server Components yet - React hooks must be declared before any early returns (ESLint requirement)
- Popups and tooltips use
createPortal(el, document.body)for z-index stacking - The TipTap editor syncs via
onUpdatewith a 500ms debounced auto-save