Storage Layers
Epics & Stories uses four storage layers, each with a different job.
Overview
| Layer | Technology | Role | Where |
|---|---|---|---|
| IDB | IndexedDB (browser) | Offline-first local store | Browser |
| D1 | Cloudflare SQLite | Server sync authority | Cloudflare |
| KV | Cloudflare KV | Fast metadata reads | Cloudflare |
| R2 | Cloudflare R2 | Durable artifact storage | Cloudflare |
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:
boardstable — board metadata with version counternotestable — all notes withdeleted_atfor soft deletesjira_itemstable — Jira work items with hierarchy and soft deletes- Version counters (
versioncolumn) 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 pattern | Contents |
|---|---|
| Session records | HttpOnly cookie → session data |
| OTP records | Short-lived login codes |
| Board access metadata | Owner + invited emails per board |
| User board index | List of boards per user (dashboard) |
| Subscription records | Stripe plan + status per user |
| Monthly usage counters | AI credits + doc uploads per user |
| Changelog indexes | Pointers to R2 changelog entries |
| Rate-limit counters | Per-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 boardsnapshots/{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
- Sync Engine — D1 push/pull mechanics
- Changelog and Snapshots — R2 recovery details
- State Management — IDB and Zustand interaction