Sync Engine (D1 Sync)
Goals
The sync engine is designed to:
- Avoid idle background polling — no background timers consuming bandwidth when the tab is inactive
- Batch writes efficiently — cosmetic changes (drag, recolour) are grouped, not sent one by one
- Keep UI responsive — the user never waits for a network round-trip to see their edits
- Resolve conflicts predictably — last-write-wins by
updatedAttimestamp
Sync strategy
Pull (server → client)
A pull is triggered:
- On initial load — fetches the current board state to hydrate the client
- On tab refocus — when
document.hiddenbecomesfalse(Page Visibility API)
Pull is rate-limited: minimum 10 seconds between pulls, so rapid tab switching doesn't flood the server.
There is no periodic polling timer. If the tab stays focused for hours, no background pull happens.
Push (client → server)
Push is triggered by local mutations. There are two tiers:
| Tier | Examples | Debounce / trigger |
|---|---|---|
| Important changes | Create note, delete note, edit content, edit title | 5-second cloud debounce after last change |
| Cosmetic changes | Drag/move note, recolour, change type tag | Batch: ≥15 changes OR 60-second backstop timer |
The distinction avoids sending a push for every pixel of a drag operation while still ensuring moves are eventually persisted.
Delta sync via version counters
All rows in D1 have a monotonically increasing version column managed by the server.
Pull request: GET /api/boards/{boardId}/sync?since={lastKnownVersion}
The server performs a lightweight version check first:
- Runs two queries to get the current max version for board + notes + Jira items.
- If the current version equals
since, returns immediately with no data (idle short-circuit). - Otherwise returns all rows with
version > since.
Push request: POST /api/boards/{boardId}/sync with a batch payload containing upserted and deleted IDs.
Conflict resolution
The merge strategy is last-write-wins by updatedAt timestamp:
if (remoteNote.updatedAt > localNote.updatedAt) {
use remote version
} else {
keep local version
}Board-level metadata merge skips if name, updatedAt, and ownerEmail are all unchanged to avoid unnecessary writes.
Jira items in sync
Jira work items are included in the same sync payload as notes:
{
"jiraItems": {
"upserted": [...],
"deleted": [...]
}
}Soft deletes are represented as upserted items with a non-null deletedAt field.
Offline behaviour
- Edits continue locally. IDB ensures they survive page reloads.
- When connectivity returns and the tab regains focus, a pull catches up missed remote changes.
- Pending pushes retry with exponential backoff on failure.
SyncManager lifecycle
| Event | Action |
|---|---|
| Component mount | init() — initial pull, register visibility listener |
| Tab comes into focus | Rate-limited pull |
| User makes important edit | schedulePush(immediate=true) — 5s debounce |
| User drags 15+ notes | schedulePush(immediate=false) — threshold flush |
| Component unmount | destroy() — final push, clear timers, remove listener |
See also
- Storage Layers — D1 and IDB roles
- State Management — Optimistic updates
- Sync API — Pull and push endpoint schemas