Technical DocumentationStore Architecture

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 view

Sync 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.onLine before 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 onUpdate with a 500ms debounced auto-save