Documentation
Architecture
Sync Engine

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 updatedAt timestamp

Sync strategy

Pull (server → client)

A pull is triggered:

  1. On initial load — fetches the current board state to hydrate the client
  2. On tab refocus — when document.hidden becomes false (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:

TierExamplesDebounce / trigger
Important changesCreate note, delete note, edit content, edit title5-second cloud debounce after last change
Cosmetic changesDrag/move note, recolour, change type tagBatch: ≥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:

  1. Runs two queries to get the current max version for board + notes + Jira items.
  2. If the current version equals since, returns immediately with no data (idle short-circuit).
  3. 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

EventAction
Component mountinit() — initial pull, register visibility listener
Tab comes into focusRate-limited pull
User makes important editschedulePush(immediate=true) — 5s debounce
User drags 15+ notesschedulePush(immediate=false) — threshold flush
Component unmountdestroy() — final push, clear timers, remove listener

See also