Documentation
Architecture
Storage Layers

Storage Layers

Epics & Stories uses four storage layers, each with a different job.

Overview

LayerTechnologyRoleWhere
IDBIndexedDB (browser)Offline-first local storeBrowser
D1Cloudflare SQLiteServer sync authorityCloudflare
KVCloudflare KVFast metadata readsCloudflare
R2Cloudflare R2Durable artifact storageCloudflare

IndexedDB (browser)

Role: Primary offline-first local store.

What it stores:

  • Board state (board metadata + notes + Jira work items)
  • Recent boards list (last 50 boards opened)
  • Per-board AI chat message history (last 100 messages)
  • Per-note TipTap editor state (crash recovery)
  • Per-Jira-item editor state (crash recovery)

Why: Instant load on refresh, works completely offline, survives page reload. All UI reads come from here — the server is never queried directly for rendering.


D1 (Cloudflare SQLite)

Role: Server-side sync authority and multi-client reconciliation.

What it stores:

  • boards table — board metadata with version counter
  • notes table — all notes with deleted_at for soft deletes
  • jira_items table — Jira work items with hierarchy and soft deletes
  • Version counters (version column) on all rows for delta sync

Why: Enables multiple clients on different devices to converge on the same state. D1 is the canonical record when two clients disagree. Last-write-wins conflict resolution uses updatedAt timestamps.

Schema migrations: Applied with wrangler d1 migrations apply. Migration files are in migrations/ in the project root.


KV (Cloudflare KV)

Role: Lightweight, fast-read metadata and entitlements store.

What it stores (conceptually):

Key patternContents
Session recordsHttpOnly cookie → session data
OTP recordsShort-lived login codes
Board access metadataOwner + invited emails per board
User board indexList of boards per user (dashboard)
Subscription recordsStripe plan + status per user
Monthly usage countersAI credits + doc uploads per user
Changelog indexesPointers to R2 changelog entries
Rate-limit countersPer-user/IP sliding window counters

Why: KV is optimised for frequent, low-latency reads with infrequent writes. Sessions, OTP checks, and quota lookups all benefit from sub-millisecond KV reads.


R2 (Cloudflare R2)

Role: Durable blob storage for snapshots and audit trail.

What it stores:

  • current.md — latest full board Markdown snapshot per board
  • snapshots/{timestamp}.md — point-in-time board snapshots
  • Changelog entries — per-note Markdown fragments with mutation history

Why: Cheap, durable, and designed for larger payloads. The audit trail and snapshot history is append-heavy and read infrequently — R2 is the ideal fit. It also serves as a recovery layer if both IDB and D1 are unavailable.

Recovery fallback

If a client has no local IDB state (first device, cleared browser, or new collaborator), BoardHydrator fetches the latest board snapshot from R2 via GET /api/boards/{boardId}/snapshot to recover the initial state.


Layer interaction on a mutation

User edits note content
  → Zustand store updated (instant)
  → IDB saved (300–1000ms debounce)
  → D1SyncManager.schedulePush() (5s cloud debounce for content changes)
  → ChangeLogger.trackContentChange() (15s debounce → R2 changelog entry)

See also