Offline-first architecture concept page. Local-first apps (Linear, Figma offline, Obsidian). IndexedDB / SQLite-WASM is source of truth, sync engine (Replicache/ElectricSQL) replays mutations to a Sync API backed by Postgres. Three scenarios: offline edit drained on reconnect, CRDT conflict merge across two devices, optimistic UI rollback when server rejects. Carries an ADR comparing Replicache vs ElectricSQL vs custom sync.
Offline-first означает, что критическая локальная задача имеет определённое поведение без сети. Это не обещание eventual success: storage может быть очищен, credentials истечь, сервер отклонить intent, а concurrent edits потребовать domain-specific merge или выбора пользователя.
Надёжная модель сначала атомарно сохраняет локальное состояние и outbox intent, затем повторяет sync с idempotency identity. Server остаётся authority для глобальных invariants и возвращает explicit version/conflict.
| Компонент | Роль в модели |
|---|---|
| Offline-capable UI | local readwrite transaction; announce durability state |
| Versioned Local Database | local readwrite transaction; same transaction intent; write reconciled version |
| Durable Operation Outbox | same transaction intent; claim pending operation |
| Restartable Sync Coordinator | claim pending operation; replay with identity; resolve explicit conflict |
| Authorized Idempotent API | replay with identity; authorize and commit |
| Authoritative Server State | authorize and commit |
| Domain Conflict Policy | resolve explicit conflict; write reconciled version |
| Accessible Sync Status | announce durability state |
While offline, the UI writes the edited record and a stable operation ID to stores in one IndexedDB readwrite transaction. It reports local durability only after transaction complete.
Проверяемый исход: A crash cannot leave a visible committed edit without its pending sync intent, and the user sees that server confirmation is still pending.
A foreground or supported background trigger starts a restartable worker. It reads one pending operation, sends idempotency and expected-version metadata, then marks acknowledged only after durable server result.
Проверяемый исход: Lost acknowledgements can cause duplicate delivery but not duplicate effect; uncertain operations remain retryable/reconcilable.
The server rejects an operation based on stale version. The coordinator invokes a domain policy: safe field merge, CRDT merge, user choice or explicit rejection; it never applies universal last-write-wins blindly.
Проверяемый исход: The reconciled local record names its server base/version and any unresolved user decision remains visible.
The platform lacks Background Sync or storage rejects a write. The app keeps a foreground/manual retry path, exposes the real durability state and never promises queued success before local commit.
Проверяемый исход: Unsupported APIs reduce convenience, not correctness; a failed local commit is shown as unsaved with recoverable user options.
arrival_rate < effective_ack_rate; temporary bursts create backlog_change ≈ (arrival_rate − ack_rate) × interval.minimum_drain_time ≥ pending_operations / sustained_ack_operations_per_second, ignoring new arrivals; include backoff and server limits in the measured sustained rate.local_bytes ≈ records + pending_payloads + indexes + engine_overhead; quota is implementation/user dependent and not a product constant.operations_rejected_for_version / version-checked_operations; last-write-wins may reduce this counter by silently losing intent, so track user-visible loss separately.Числа и bounds выше действуют только при названных units, population и assumptions. Ни паттерн, ни browser API сами по себе не задают SLA, capacity или correctness.
Диаграмма показывает offline mutation lifecycle. Она не гарантирует permanent device storage, automatic background execution, multi-device convergence, end-to-end encryption или acceptance сервером каждого локального intent.
Введите числа или выберите пресет