# Offline-Capable Client with Cursor Pagination — PRD

## Status

In Progress

## Executive Summary

This initiative transforms the Coda client from a stateless, online-only chat interface into an offline-capable, multi-tab-aware application backed by IndexedDB, with cursor-based pagination replacing the current offset-based approach. The result is a client that loads instantly from local data, works without a network connection for key operations (browsing, starring, renaming, deleting chats), synchronizes reliably across tabs and devices, and scales gracefully to users with hundreds of conversations.

## The Problem

Meet Sara, a royalty analyst at a mid-size label group. She uses Coda daily to investigate advance recoupment schedules, reconcile ledger balances across territories, and pull revenue breakdowns for quarterly reviews. Her workflow depends on fast, reliable access to her conversation history — she routinely references previous queries to compare period-over-period figures.

Here is what Sara's Tuesday looks like:

**9:02 AM** — Sara opens Coda to continue yesterday's deep-dive into a complex multi-territory advance. The page loads. The entire chat list fetches from the server. Every message in the conversation fetches from the server. She waits. The office Wi-Fi is sluggish this morning — three seconds, five seconds, eight seconds staring at a loading spinner. She had this exact conversation open twelve hours ago. The data was right there. Now it is gone, because the client stores nothing locally beyond the current browser session.

**9:47 AM** — Sara has Coda open in two browser tabs — one for the advance investigation, one for a separate revenue query she is running in parallel. She stars the advance conversation in Tab A so she can find it quickly later. Tab B knows nothing about this. She switches to Tab B, scrolls through her sidebar, and the star is missing. She stars it again. Later, when both tabs refresh, the duplicate action is harmless — but the confusion is not. The tabs are two isolated islands with no awareness of each other.

**10:15 AM** — Sara needs to scroll back through a long conversation — 140 messages deep — to find a specific ledger breakdown the agent provided last week. The current pagination uses numeric offsets. She scrolls, the client requests page 2, page 3. Meanwhile, a new message arrives from another device, shifting every item's position by one. Page 4 returns a duplicate message she already saw. Page 5 skips one she has not. The offset-based pagination has silently corrupted her view of the conversation, and she does not even realize she missed something.

**11:30 AM** — Sara is on a video call reviewing royalty statements with a client services manager. She needs to pull up a Coda conversation to reference a specific calculation. She clicks. The network request takes four seconds. Dead air on the call. "Sorry, just loading..." This is a tool she used five minutes ago. The data should be instant.

**2:00 PM** — Sara is on the subway heading to a meeting, reviewing notes on her phone. She opens Coda to check a starred conversation. No network. Blank screen. The entire application is unusable offline — not because she needs to send new queries, but because the client cannot even display data it already fetched and rendered an hour ago.

**4:30 PM** — Sara renames a conversation from "Q4 advance check" to "Q4 FY26 advance recoupment — ACME Records" so she can find it later. The network request fails silently (brief connectivity drop). She does not notice. Tomorrow morning, the old title is back. Her organizational work is lost.

These are not edge cases. They are the daily experience of every Coda user. The root causes are structural:

1. **Zero local persistence.** The client uses localStorage for a handful of config flags but stores no chat or message data locally. Every page load, every tab, every navigation starts from zero.
2. **Offset-based pagination.** Numeric offsets break when the underlying data set changes between page requests — a fundamental flaw for any data set that is actively updated.
3. **No cross-tab coordination.** Each tab maintains its own independent state with no mechanism to share changes.
4. **No offline capability.** The application is entirely server-dependent for rendering previously-seen content.

## The Opportunity

After this work ships, Sara's Tuesday looks different:

She opens Coda and her chat list appears instantly — rendered from IndexedDB before the network request even completes. Her starred conversations are right where she left them. She clicks into yesterday's advance investigation and the last 50 messages render immediately from cache; background sync quietly fetches any new messages that arrived overnight.

She works in two tabs without thinking about it. When she stars a conversation in one tab, it appears starred in the other within seconds — a leader tab coordinates synchronization, and BroadcastChannel notifies followers to re-read from the shared IndexedDB store.

She scrolls back through a long conversation and pagination works correctly every time — cursor-based keyset queries are immune to the data-shifting problem that plagued offset pagination.

On the subway, she opens Coda and browses her cached conversations, renames one, stars another. These mutations queue locally. When she surfaces and reconnects, they flush to the server automatically. If someone else renamed the same conversation in the meantime, she gets a brief toast notification and the server's version wins — no silent data loss.

The application feels native. It feels fast. It feels like it respects the work she has already done.

## Goals & Success Criteria

| Metric                                         | Current                      | Target                                                   |
| ---------------------------------------------- | ---------------------------- | -------------------------------------------------------- |
| Chat list time-to-render (warm)                | 1-4s (full server fetch)     | <200ms (IndexedDB read)                                  |
| Message history time-to-render (warm)          | 2-8s (full server fetch)     | <300ms (IndexedDB read)                                  |
| Offline browse capability                      | None (blank screen)          | Full browse of cached chats and messages                 |
| Offline mutation support                       | None (actions fail silently) | Star, unstar, rename, delete queue and sync              |
| Cross-tab consistency                          | None (independent state)     | Changes propagate within 3s via BroadcastChannel         |
| Pagination correctness under concurrent writes | Broken (offset shift)        | Correct (cursor-based keyset, immune to inserts/deletes) |
| Data loss from transient network failures      | Possible (silent failures)   | Zero (mutations persist locally, sync on reconnect)      |

## User Stories

1. **As a royalty analyst**, I want my chat list to appear instantly when I open Coda, so that I do not waste time waiting for the server on every page load.

2. **As a royalty analyst**, I want to browse my previously loaded conversations while offline (subway, airplane, spotty office Wi-Fi), so that I can reference past queries and results without a network connection.

3. **As a royalty analyst**, I want to star, rename, and delete conversations while offline, so that my organizational work is not blocked by connectivity.

4. **As a royalty analyst**, I want my offline changes to sync automatically when I reconnect, so that I never have to redo work or remember what I changed.

5. **As a royalty analyst**, I want scrolling through long message histories to work correctly even when new messages arrive, so that I never see duplicates or miss messages.

6. **As a royalty analyst**, I want changes I make in one browser tab to appear in my other open tabs within seconds, so that I do not have to manually refresh or encounter stale state.

7. **As a royalty analyst**, I want to be notified when a change I made conflicts with a change from another device, so that I understand what happened instead of silently losing data.

8. **As a royalty analyst**, I want to drag-and-drop reorder my starred conversations, so that I can organize my most important chats in the order that matches my workflow.

## Proposed Solution

Replace the current stateless, localStorage-based client architecture with an IndexedDB-backed offline-capable system consisting of four core modules:

- **IndexedDB data layer (CodaStore):** Structured local storage for chats, messages, attachment metadata, a mutation queue, and sync state. Serves as the single source of truth for all UI rendering. Includes tiered eviction to manage storage quotas.

- **Cursor-based pagination:** Server endpoints migrate from offset-based to Relay-style cursor pagination using opaque, base64url-encoded cursors with keyset queries. Immune to data-shift problems. Supports bidirectional traversal (load-more and delta sync) on both the chat list and message history.

- **Sync infrastructure:** A leader-elected tab polls the server using conditional ETags, performs delta sync via cursors, and notifies follower tabs via BroadcastChannel. Mutations are applied optimistically, queued in IndexedDB, and flushed to the server with compare-and-swap concurrency control.

- **Cross-tab coordination:** BroadcastChannel-based messaging with a visibility-aware leader election protocol ensures exactly one tab performs network sync while all tabs share the same IndexedDB state.

## User Experience

**What changes:**

- **Instant warm loads.** Returning to Coda renders cached data immediately. A subtle sync indicator shows when background refresh is in progress.
- **Offline banner.** When the network is unavailable, a non-intrusive bar appears: "You're offline. Changes will sync when you reconnect." All cached content remains browsable. Star, rename, and delete actions work normally.
- **Reconnect toast.** When connectivity returns: "Back online. Syncing..." Queued mutations flush automatically.
- **Conflict notification.** If an offline mutation conflicts with a server-side change (e.g., another device renamed the same chat): "Some changes were updated from another device." Server state wins. No silent data loss.
- **Scroll-up pagination.** Long message histories display a "Load earlier messages" control at the top. Loads are fast and correct regardless of concurrent activity.
- **Attachment placeholders.** Attachment metadata renders inline (icon, filename, size). Binary content loads on demand via pre-signed S3 URLs and caches locally.
- **Starred chat reordering.** Drag-and-drop reorder of starred chats in the sidebar persists locally and syncs across tabs instantly.

**What stays the same:**

- The core chat interaction — sending messages, receiving streamed AI responses via SSE — is unchanged.
- Authentication flow is unchanged.
- All existing UI components retain their visual design; changes are limited to data source wiring.

## Benefits

### Business benefits

- **Reduced churn risk.** A snappy, offline-capable tool increases daily engagement and reduces frustration-driven abandonment. Users who interact with Coda on mobile or in low-connectivity environments (a meaningful segment) gain an entirely new capability.
- **Competitive differentiation.** Most internal AI chat tools are stateless web apps. Offline capability and instant loads set Coda apart as a serious productivity tool, not a demo.
- **Reduced support burden.** Eliminating offset pagination bugs, silent mutation failures, and cross-tab inconsistencies removes several categories of user-reported issues.

### User benefits

- **Sub-200ms warm loads** vs. multi-second server fetches — a 10-20x improvement in perceived performance.
- **Offline access** to previously viewed conversations and the ability to organize them.
- **Zero data loss** from transient network failures — mutations survive connectivity gaps.
- **Cross-tab consistency** — no more conflicting state between browser tabs.
- **Correct pagination** — no more duplicate or missing messages when scrolling through long histories.

### Engineering benefits

- **Cursor pagination** is a well-understood, industry-standard pattern that eliminates an entire class of data consistency bugs.
- **IndexedDB data layer** provides a clean foundation for future features (full-text search, conversation export, advanced caching).
- **Leader election + BroadcastChannel** is a reusable pattern for any future cross-tab coordination needs.
- **Optimistic concurrency control** (compare-and-swap via `updatedAt`) is simple, stateless, and avoids the complexity of distributed locks or version vectors.

## Costs

### Engineering effort

| Layer                               | Description                                               | Estimate             |
| ----------------------------------- | --------------------------------------------------------- | -------------------- |
| Layer 1: Shared API types           | Pagination types, updated contracts, client methods       | 2 person-days (done) |
| Layer 2: Server cursor pagination   | Keyset queries, cursor encoding, 4 endpoints              | 5 person-days        |
| Layer 3: Server ETag + concurrency  | ETag generation, 304 handling, CAS on mutations           | 2 person-days        |
| Layer 4: Client IndexedDB layer     | CodaStore, schema, eviction, subscriptions                | 4 person-days (done) |
| Layer 5: Client sync infrastructure | BroadcastRelay, LeaderElection, SyncEngine, MutationQueue | 6 person-days        |
| Layer 6: Client React hooks + UI    | Hooks, providers, component rewiring, UI indicators       | 5 person-days        |
| **Total**                           |                                                           | **~24 person-days**  |

Layers 1 and 4 are complete. Remaining work: ~17 person-days.

### Infrastructure costs

- **IndexedDB storage:** Client-side, zero server cost. Browser-managed quotas (~50MB-1GB depending on browser/device).
- **S3 attachment bucket:** Required prerequisite. Storage cost is proportional to attachment volume — expected to be modest (attachments are optional and uncommon today). Pre-signed URL generation is a local CPU operation (HMAC), no per-request AWS cost.
- **No new server infrastructure.** ETag computation uses existing Redis data. Cursor pagination uses existing database indexes (one new index required for soft-delete sync).

### Maintenance burden

- IndexedDB schema versioning requires migration support for future schema changes (standard `idb` library handles this).
- Leader election protocol needs monitoring — incorrect leader behavior (stuck leader, no leader) would degrade sync. The heartbeat/stale-detection design is self-healing but should be instrumented.
- Eviction thresholds (60%/85% of quota, TTL values) may need tuning based on real-world usage patterns.

### Opportunity cost

The estimated 17 remaining person-days could alternatively fund:

- 2-3 new agent tool integrations
- A full-text search feature (though this work's IndexedDB layer makes client-side search cheaper to build later)
- Additional agent capabilities (document generation, export)

The offline/pagination work is foundational infrastructure. Deferring it means every future feature inherits the current fragile data layer, compounding the cost of not doing it.

## Dependencies

| Dependency                                                        | Status          | Blocking?                                |
| ----------------------------------------------------------------- | --------------- | ---------------------------------------- |
| S3 bucket for attachment storage (per environment)                | Not configured  | Blocks attachment URL endpoint (Layer 2) |
| S3 CORS configuration for client-origin pre-signed URL fetches    | Not configured  | Blocks client attachment fetching        |
| IAM role / credentials for S3 pre-signed URL signing              | Not configured  | Blocks attachment URL endpoint           |
| Database index `idx_chats_user_deleted (userId, deletedAt)`       | Not yet added   | Blocks deleted chats endpoint (Layer 2)  |
| Server soft-delete retention period policy (recommended: 60 days) | Decision needed | Blocks hard-delete cleanup job           |
| Client eviction TTL policy (recommended: 30 days)                 | Decision needed | Blocks eviction implementation tuning    |
| Attachment soft-delete cascade verification                       | Not verified    | Blocks S3 cleanup job                    |

## Risks & Mitigations

| Risk                                                          | Impact                                            | Likelihood                        | Mitigation                                                                                                                                                                                                   |
| ------------------------------------------------------------- | ------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **IndexedDB quota exceeded**                                  | Writes fail, app degrades to online-only          | Low (browsers allocate 50MB-1GB+) | Three-tier eviction strategy (soft at 60%, hard at 85%, ceiling fallback). Priority ordering preserves starred content longest. Graceful degradation — app continues working online-only if storage is full. |
| **IndexedDB unavailable** (private browsing, disabled)        | No offline capability                             | Low                               | Feature-detect on startup. Fall back to online-only mode (current behavior). No worse than today.                                                                                                            |
| **Sync conflicts** (same chat mutated on two devices offline) | User sees unexpected state                        | Medium                            | Compare-and-swap via `ifUpdatedAt` field. Server rejects stale mutations (409 Conflict). Client fetches latest state and shows toast notification. Server always wins — simple, predictable.                 |
| **Stale leader tab** (leader tab crashes without resigning)   | Sync pauses for up to 8 seconds                   | Low                               | Heartbeat-based stale detection (8s threshold). Any visible follower tab claims leadership with jitter-based tie-breaking. Self-healing by design.                                                           |
| **BroadcastChannel not supported** (very old browsers)        | No cross-tab sync                                 | Very low (97%+ support)           | Feature-detect. Each tab syncs independently — slightly more server load but functionally correct.                                                                                                           |
| **Data divergence between IndexedDB and server**              | User sees outdated content                        | Low                               | ETag-based change detection on every poll cycle. Delta sync via cursors ensures eventual consistency. Client eviction TTL < server retention period ensures no orphaned local data.                          |
| **Branch divergence on warm tab**                             | User does not see new messages from branch switch | Low                               | By design — warm tabs keep their branch. Cold load (refresh) picks up the active branch. Prevents jarring automatic branch switches during active use.                                                       |
| **S3 pre-signed URL expiration**                              | Attachment fetch fails                            | Low                               | Client fetches fresh URL on demand from the attachment URL endpoint. Cached blobs in IndexedDB bypass URL entirely on subsequent views.                                                                      |

## Timeline & Milestones

| Phase       | Layers                                    | Description                                                             | Status      | Target   |
| ----------- | ----------------------------------------- | ----------------------------------------------------------------------- | ----------- | -------- |
| **Phase 1** | Layer 1 (API types) + Layer 4 (IndexedDB) | Foundation: shared types and local data layer                           | Done        | --       |
| **Phase 2** | Layer 2 (Server pagination)               | Cursor-based endpoints, deleted chats endpoint, attachment URL endpoint | Not started | Week 1-2 |
| **Phase 3** | Layer 3 (Server ETag + CAS)               | Conditional polling support and optimistic concurrency                  | Not started | Week 2   |
| **Phase 4** | Layer 5 (Client sync)                     | BroadcastRelay, LeaderElection, SyncEngine, MutationQueue               | Not started | Week 3-4 |
| **Phase 5** | Layer 6 (React hooks + UI)                | Hook implementation, component rewiring, UI indicators                  | Not started | Week 4-5 |
| **Phase 6** | Integration + polish                      | End-to-end testing, eviction tuning, browser compatibility              | Not started | Week 5-6 |

Prerequisites (S3 setup, DB index, retention policies) should be resolved during Phase 2 at the latest.

## Open Questions

1. **Conflict toast specificity.** The current design shows a generic "Some changes were updated from another device" toast. Should we invest in specific messages ("Chat renamed on another device", "Star removed on another device")? This requires passing mutation type and chat title through the conflict notification path.

2. **Attachment backfill.** How should existing attachments stored as base64 inline (if any exist) be handled when the S3 storage path is implemented? Migration, dual-read, or ignore?

3. **Pre-signed URL error response for soft-deleted attachments.** Should the attachment URL endpoint return 404 (standard) or 410 Gone (semantically richer) for soft-deleted attachments?

4. **Eviction TTL tuning.** The spec proposes 14 days for blobs, 30 days for non-starred chats, 60 days for starred chats. These are reasonable defaults but may need adjustment based on real usage. Should we instrument eviction events to inform tuning?

5. **localStorage migration timing.** The spec defers migrating non-chat localStorage keys (feature flags, UI preferences) to IndexedDB. When should this follow-up be scheduled — immediately after launch, or as a separate initiative?

6. **CloudFront for S3 attachments.** The spec notes CloudFront is optional. At what usage threshold should we revisit this decision?

7. **SSE/WebSocket upgrade path.** The spec uses polling with ETags. If real-time sync becomes a requirement (e.g., collaborative viewing), what is the trigger and effort estimate for upgrading to push-based sync?
