RFC: Offline-First Transactions #554
Replies: 6 comments 14 replies
-
Looks great 👍 A few observations:
|
Beta Was this translation helpful? Give feedback.
-
I like that this is possible, be as @thruflo mentions with the existing api, maybe a flag would be nice to turn on/off. And I assume a collection can therefore only be registered with one offline mutation executor.
This is going to be complex, Reading this through, my biggest concern is that without also having the sync managed through a leader election, sharing the offline mutations between tabs could results in some strange broken state or race conditions. A transaction that is awaiting a sync back, could be "applied" by the leader tab before the sync has caught up on other tabs. |
Beta Was this translation helpful? Give feedback.
-
Should also say other than that its looking good! |
Beta Was this translation helpful? Give feedback.
-
I don't understand the description of keys. Can you describe the "global keys" concept in more detail or point me towards something that specifies it? My main apprehension as currently described is that causal dependencies between transactions may not be reflected in these keys. For example in Notion's transaction system, we can have causal dependencies between transactions:
|
Beta Was this translation helpful? Give feedback.
-
Great RFC. Thanks for looking into this ❤️ A few questions:
|
Beta Was this translation helpful? Give feedback.
-
TODO, consider how this will work with Partitioned collections #315 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
(Note: This RFC covers offline mutation persistence and retry - ensuring writes don't get lost when offline. This is distinct from offline data persistence, which involves caching/syncing read data for offline access.)
Summary
TanStack DB applications will persist all mutations to a durable outbox before dispatch, enabling automatic replay when connectivity is restored. The system provides per-key scheduling (parallel across distinct keys, sequential per key), exponential backoff with jitter, failure discrimination via NonRetriableError, and developer hooks for filtering and squashing operations. Optimistic state is restored on restart by replaying persisted transactions, ensuring users never lose work during offline periods.
Background
TanStack DB provides reactive client store with collections, live queries, and optimistic mutations. Currently, when a transaction's mutation function fails, the optimistic state is rolled back and the operation is lost. Users must manually retry operations when connectivity returns.
The framework lacks built-in mechanisms for persisting failed transactions across application restarts, automatically retrying operations when connectivity is restored, or distinguishing between temporary failures (network issues) and permanent failures (validation errors).
Demand for offline-first capabilities spans field service applications, productivity tools, mobile applications, and local-first collaborative systems. Without first-class offline support, developers must either accept data loss during network failures or build complex custom persistence and retry logic outside of TanStack DB.
Problem
Developers using TanStack DB cannot build reliable offline-first applications without significant custom code. This creates three critical problems:
Data Loss During Network Failures: When a transaction's mutation function fails due to network issues, the optimistic updates are rolled back and the user's changes are lost. Users must remember and manually re-enter their data when connectivity returns, leading to frustration and potential data inconsistencies.
No Persistence Across Application Restarts: If the application closes while offline (browser tab closed, mobile app backgrounded, device restarted), any pending operations are permanently lost. There is no mechanism to queue and retry these operations when the application restarts with connectivity.
Inability to Distinguish Failure Types: All errors are treated identically - whether it's a temporary network failure that should be retried or a permanent validation error that will never succeed. This leads to either wasted resources retrying operations that will always fail or premature abandonment of operations that would succeed with retry.
These problems make TanStack DB unsuitable for applications requiring reliable offline operation, forcing developers to either accept data loss or build complex workarounds outside the framework.
Proposal
Core Architecture
Implement an outbox-first persistence system where every offline transaction is stored to durable storage before dispatch. This builds on TanStack DB's existing transaction model by adding persistence and replay capabilities to the current
Transaction
class.Outbox Schema
Each outbox transaction extends the existing
Transaction
format:Storage Adapter
The storage layer accepts JavaScript objects and handles serialization internally. Default implementation uses IndexedDB for browsers with fallback to localStorage. Storage quota exceeded errors are thrown to the application for handling.
Intelligent Execution Scheduling
The executor implements per-key scheduling based on
PendingMutation.globalKey
:maxConcurrency
(default 4)globalKey
from mutations (format:${collection.id}:${itemKey}
)Retry Policy
Implements infinite retry with exponential backoff:
Failure Discrimination
Online Detection and Triggers
Retry execution triggers on:
navigator.onLine
eventsnotifyOnline()
Developer Control Hooks
beforeRetry Hook: Called before each retry batch with
beforeRetry(transactions[])
. Can filter, transform, or squash transactions. Filtered transactions are permanently deleted from storage.Manual Management:
removeFromOutbox(id)
for programmatic transaction removal. OptionalpeekOutbox()
for diagnostics.Optimistic State Restoration
On application restart, persisted transactions replay through the existing transaction system by calling the normal collection operations (insert/update/delete) to restore optimistic UI state. This leverages the existing
PendingMutation
data structure.Multi-Tab Coordination
Only one executor runs per origin using:
Leadership transitions handle edge cases gracefully, with delays acceptable for v1 implementation.
API Design
Executor Initialization with Mutator and Collection Registry:
Offline Transaction Creation (follows
createTransaction
pattern):Offline Action Creation (follows
createOptimisticAction
pattern):Automatic Offline Support for Collection Operations
When collections are registered with the offline executor, their existing mutation APIs automatically gain offline capabilities:
Transparent Offline Behavior:
Behind the Scenes:
todoCollection
is registered in the collection registry, these calls automatically create offline transactions'todos'
) as themutatorName
onInsert
/onUpdate
/onDelete
)Explicit Transactions (for custom mutators):
Integration with Existing System
This builds directly on the existing transaction system:
Transaction
class: Extends rather than replaces current transaction modelPendingMutation
: Uses existing mutation data structure for persistenceDefinition of success
This proposal succeeds when developers can build reliable offline-first applications with TanStack DB without custom persistence logic. Success metrics include:
Functional Requirements Met:
Developer Experience Goals:
createTransaction
,createOptimisticAction
)Performance Targets:
Reliability Standards:
The feature succeeds when developers can add offline capabilities to existing TanStack DB applications with minimal code changes while maintaining the framework's reactive performance characteristics.
Comparison with Existing Solutions
mutationFn
availableTransaction
withPendingMutation[]
bound by mutator registry; no closuresmutatorName
+ JSON args; deterministic; re-runnableglobalKey
PendingMutation.globalKey
Retry-After
NonRetriableError
drops transactionbeforeRetry(transactions[])
,removeFromOutbox
, optionalpeekOutbox()
startOfflineExecutor({ mutators })
,offline.createOfflineTransaction
,offline.createOfflineAction
persistQueryClient
+ mutation defaultsrep.mutate.<name>(args)
; server sync protocolTransaction
andPendingMutation
Key Differentiators
vs TanStack Query: TanStack DB Offline Transactions provides key-aware scheduling and true transaction persistence across restarts, rather than just pausing individual mutations. The integration is native since it extends the existing transaction system.
vs Redux-Offline: Built-in parallelism via automatic key derivation eliminates the need for custom middleware. The system integrates directly with TanStack DB's reactive collections and optimistic updates.
vs Replicache: Focused on write-path reliability rather than full local-first database replacement. Developers keep their existing backend architecture while gaining offline write resilience.
Positioning: TanStack DB Offline Transactions extends your existing TanStack DB transaction workflow with durable outbox semantics and intelligent retry scheduling. It doesn't replace your backend, conflict strategy, or sync engine - it makes your existing write path safe under flaky networks and app restarts, with minimal API surface area.
Beta Was this translation helpful? Give feedback.
All reactions