Sync Engine
Sync Engine
Offline-first cloud synchronization with conflict detection, delta sync, and real-time updates
Overview
The Sync Engine enables cloud synchronization for authenticated users, ensuring data is backed up and accessible across devices. It implements an offline-first architecture with change tracking, delta sync via SHA-256 hash comparison, LZ compression for wire efficiency, an outbox pattern for reliable delivery, and real-time updates via Supabase Realtime channels.
Requirements
Functional Requirements
| ID | Requirement | Acceptance Criteria | Priority | Source |
|---|---|---|---|---|
| REQ-SYN-001 | The system shall track local changes and batch-sync them to Supabase when authenticated | Edit data → change tracked; sync triggers → data appears in cloud; offline edits → queued in outbox | Must | syncEngine |
| REQ-SYN-002 | The system shall detect conflicts when the same entity is modified locally and remotely | Edit on device A + B → sync → conflict detected; conflict UI shows both versions; user chooses resolution | Must | syncEngineConflicts |
| REQ-SYN-003 | The system shall use delta sync with SHA-256 hashing to minimize data transfer | Edit 1 field → only changed entity synced (not full dataset); hash match → skip; hash mismatch → sync | Must | diff |
| REQ-SYN-004 | The system shall compress sync payloads over 10 KB with the browser's native CompressionStream (gzip) | Payload > COMPRESSION_THRESHOLD (10 KB) → gzipped before send; smaller payloads sent as-is; a peer-pushed payload decompressing above MAX_DECOMPRESSED_BYTES (10 MB) is rejected | Should | compression |
| REQ-SYN-005 | The system shall persist pending changes in an outbox that survives page reloads | Make offline edit → close tab → reopen → outbox still has pending change; reconnect → change synced | Must | outbox |
| REQ-SYN-006 | The system shall display sync status (syncing, synced, error, offline) in the UI | Badge → shows current state; sync in progress → spinner; error → error styling and the detail line. The badge is role="status", not a control | Must | SyncStatusBadge |
| REQ-SYN-007 | The system shall perform a full state reconciliation on login to detect and resolve divergence | Login → full sync cycle; local-only data → pushed; remote-only data → pulled; conflicts → queued for resolution | Must | loginSync |
| REQ-SYN-008 | The system shall receive real-time updates from other devices via Supabase Realtime channels | Edit on device A → device B receives update within 2s; UI updates without manual refresh | Should | syncSubscriptions |
| REQ-SYN-009 | The system shall sync every entity type in ALLOWED_ENTITY_TYPES (96 of them — projects, templates, contacts, owned gear, the device library, accounting, banking and more) | Each entity type → syncs independently; partial failure → other types unaffected; entity routing correct | Must | syncEntityRoutes |
Non-Functional Requirements
| ID | Requirement | Metric | Priority |
|---|---|---|---|
| NFR-SYN-001 | Sync batch operations shall complete within 5s for ≤50 entities | Latency | Should |
| NFR-SYN-002 | Outbox shall handle ≤500 pending changes without performance degradation | Capacity | Should |
| NFR-SYN-003 | Real-time updates shall arrive within 2s under normal network conditions | Latency | Should |
Data Requirements
- Sync entity: Wraps any entity with hash, version, timestamps, and compression flag
- Outbox entry: Queued operation (upsert/delete) with retry counter
- Supabase table:
sync_entitiesfor cloud state,apply_sync_batchRPC for atomic writes - Calendar events are the exception: they do not travel through
sync_entities. They have their own table (calendar_events), their own metadata table (calendar_sync_metadata) and their own realtime subscription, and are upserted with deterministic UUIDv5 ids derived from user + project + type + index. This page said they synced as nested project metadata until 2026-09-07; the opposite is true — they are first-class rows, generated from a project's schedule. - Autosave debounce:
AUTOSAVE_DELAY= 700 ms to coalesce rapid edits, withMAX_AUTOSAVE_DELAY= 5 s as the ceiling so a continuous stream of edits still gets written
Constraints & Limits
- Requires authentication — sync is disabled for anonymous users
- Supabase Realtime limited to active subscription channels
- Batch size limited to prevent RPC timeout (large batches split automatically)
Offline Behavior
- Outbox pattern: Changes queued locally, synced on reconnection
- No data loss: Outbox persists across page reloads and browser restarts
- Conflict detection: On reconnect, hash comparison detects divergence
Dependencies
- Requires: Authentication (REQ-AUTH-*) — sync requires login
- Requires: Data Storage (REQ-DST-*) — reads/writes via
storageService - Required by: Collaboration (REQ-COL-*) — shared project sync
- Required by: Conflict Resolution (REQ-CFR-*) — conflict UI
How Sync Works
Automatic Sync
When signed in, sync happens automatically:
- Edit locally → change tracked with 700ms debounce
- Batch collected → pending changes queued in outbox
- Sync cycle → outbox drained, deltas computed, payload compressed
- Server writes → atomic transaction via
apply_sync_batch - Confirmation → outbox entries removed on success
[!NOTE] Delete operations are flushed immediately (no debounce) to prevent the deleted item from being resurrected by an incoming remote sync. Additionally, on page unload all pending entity changes — including owned gear, rental transactions, and timeline events — are flushed synchronously to IndexedDB to prevent data loss.
Sync Status
The sync badge in the header shows:
| Status | Meaning |
|---|---|
| Syncing… | Changes are still on their way — a push in flight, or rows still queued |
| Synced | All changes synced — also the state shown when the status is not recognised |
| Sync Error | "Changes are saved locally and will retry automatically" |
| Offline | No network — changes queued in the outbox |
There is no "Idle" state, and the badge is not a button.
SYNC_STATUShas exactly four members (syncing,synced,error,offline); this page listed a fifth until 2026-09-07. The badge renders asrole="status"with no click handler — a failed sync retries on its own, so there is nothing to click. Nothing is lost in the meantime: the changes are already in the local outbox.
Resolving Conflicts
When the same item is edited on multiple devices:
- Conflict notification appears
- Click to open the conflict resolution UI
- Compare local vs. remote versions side by side
- Choose which version to keep (or merge manually)
Data Retention & Cleanup
To ensure efficient synchronization and optimize device storage, the Sync Engine automatically enforces data retention limits:
- Soft-Deleted Entities: A deleted item is kept as a tombstone and hard-deleted from the cloud
90 days after
deleted_at— not 30, which this page claimed until 2026-09-07. The window is set inrun_data_retention_cleanup()and is deliberately the same 90 days used for the other tombstones (project activity logs, closed invites, withdrawn consent rows). - Search History: The global search history is capped at the 10 most recent queries
(
MAX_HISTORYinuseSearchHistory.js); saved searches are capped separately at 20. - Project Chat: Chat messages within collaborative projects are bounded by retention limits to maintain real-time performance.
- Sync Metadata: Orphaned or stale synchronization metadata is periodically reaped to prevent storage bloat.
Tips
- Sign in for backup: Only authenticated users get cloud sync
- Check the status badge: Ensure changes are synced before switching devices
- Resolve conflicts promptly: Unresolved conflicts block further sync for that entity
- Use on stable connections: Large initial syncs work best on WiFi
Related Documentation
Last Updated: 2026-09-07 Version: 0.790.2
