···11+# Sync Source Refactor: Replace syncSource with Device IDs
22+33+Research doc — Feb 2026
44+55+## Problem
66+77+`syncSource` is overloaded with two jobs:
88+99+1. **Origin tracking** — where did this item come from?
1010+2. **Sync gating** — should this item be pushed to the server?
1111+1212+This causes several issues:
1313+1414+- Items imported from browser history/tabs/bookmarks are silently excluded from sync (by convention, not by design)
1515+- Items pulled from the server lose their original source (overwritten with `'server'`)
1616+- The empty-string convention (`syncSource = ''` means "eligible for push") is implicit and fragile
1717+- Desktop navigation-tracked URLs sync with `syncSource = ''`, same as manual saves — making them indistinguishable on other devices
1818+1919+The fix: **everything syncs**, using the deterministic timestamp-based algorithm we already have. Device IDs (already partially implemented) become the source-of-truth for item origin.
2020+2121+## Current State
2222+2323+### syncSource values in the wild
2424+2525+| Value | Set by | Meaning | Syncs? |
2626+|-------|--------|---------|--------|
2727+| `''` (empty) | Default on item creation | Locally created, never synced | Yes (first push) |
2828+| `'server'` | Sync pull/push | Came from or was pushed to server | Only if modified after last sync |
2929+| `'history'` | Browser extension | Imported from browser history | Yes (empty string check passes*) |
3030+| `'tab'` | Browser extension | Imported from open tabs | Yes* |
3131+| `'bookmark'` | Browser extension | Imported from bookmarks | Yes* |
3232+3333+*Note: Extension imports have non-empty syncSource but `syncedAt = 0`, so they match the push query on incremental sync (`syncSource = '' OR (syncedAt > 0 AND updatedAt > syncedAt)`) — the first condition fails, the second fails too since `syncedAt = 0`. On full sync, only `syncSource = ''` items are pushed. So **extension imports are effectively blocked from syncing**, which was the intent but is encoded implicitly.
3434+3535+### Where syncSource is read/written
3636+3737+**Sync algorithm (push filtering):**
3838+- `backend/electron/sync.ts:515-525` — WHERE clause filters by `syncSource = ''`
3939+- `sync/sync.js:102-107` — Same filter in unified sync engine
4040+- `backend/tauri/src-tauri/src/sync.rs:634-665` — Same filter in Tauri desktop
4141+4242+**Set on push (marks as synced):**
4343+- `backend/electron/sync.ts:619-620` — `syncSource = 'server'` after push
4444+- `sync/sync.js:162-166` — Same
4545+- `backend/tauri/src-tauri/src/sync.rs:799-800` — Same
4646+4747+**Set on pull (marks origin as server):**
4848+- `backend/electron/sync.ts:393-394` — `syncSource = 'server'` on new items from server
4949+- `sync/sync.js:399-400` — Same
5050+- `backend/tauri/src-tauri/src/sync.rs:521-522` — Same
5151+5252+**Reset on server change:**
5353+- `backend/electron/sync.ts:667-668` — Resets all to `syncSource = ''`
5454+- `sync/sync.js:277-282` — Same
5555+5656+**Browser extension (sets import source):**
5757+- `backend/extension/history.js:119` — `syncSource = 'history'`
5858+- `backend/extension/tabs.js:138` — `syncSource = 'tab'`
5959+- `backend/extension/bookmarks.js:84` — `syncSource = 'bookmark'`
6060+6161+**Datastore (gates device metadata):**
6262+- `backend/electron/datastore.ts:2322-2324` — Skips `addDeviceMetadata()` if `syncSource` is present
6363+- `backend/electron/datastore.ts:2404-2406` — Same for updates
6464+6565+**Sync status UI:**
6666+- `backend/electron/sync.ts:773-775` — Counts pending items using syncSource filter
6767+- Extension stats pages count items by syncSource value
6868+6969+### Device ID (already partially implemented)
7070+7171+`backend/electron/device.ts` generates device IDs and adds them to item metadata:
7272+7373+```
7474+metadata._sync = {
7575+ createdBy: "desktop-550e8400-e29b-41d4-a716-446655440000",
7676+ createdAt: 1767023561234,
7777+ modifiedBy: "desktop-550e8400-e29b-41d4-a716-446655440000",
7878+ modifiedAt: 1767023561234
7979+}
8080+```
8181+8282+This metadata **survives sync** (it's inside the JSON blob), unlike `syncSource` which gets overwritten.
8383+8484+### Browser import classifications
8585+8686+Already stored redundantly — **removing syncSource loses nothing**:
8787+8888+- **Tags**: `from:history`, `from:tab`, `from:bookmark` — auto-applied on import
8989+- **Metadata JSON**: Rich browser-specific data (visit counts, tab properties, bookmark dates)
9090+9191+## Changes
9292+9393+### 1. Remove syncSource from sync algorithm
9494+9595+**Push query** — replace syncSource filter with timestamp-based:
9696+9797+Before:
9898+```sql
9999+WHERE (deletedAt = 0 AND (syncSource = '' OR (syncedAt > 0 AND updatedAt > syncedAt)))
100100+```
101101+102102+After:
103103+```sql
104104+WHERE (deletedAt = 0 AND (syncedAt = 0 OR updatedAt > syncedAt))
105105+```
106106+107107+This means: push items that have never been synced (`syncedAt = 0`) or that were modified since last sync. No reference to syncSource.
108108+109109+**After push** — only update `syncId` and `syncedAt`, stop setting `syncSource`:
110110+111111+Before:
112112+```sql
113113+UPDATE items SET syncId = ?, syncSource = 'server', syncedAt = ? WHERE id = ?
114114+```
115115+116116+After:
117117+```sql
118118+UPDATE items SET syncId = ?, syncedAt = ? WHERE id = ?
119119+```
120120+121121+**On pull** — stop setting `syncSource = 'server'` on new items. Device metadata (`_sync.createdBy`) already tracks origin.
122122+123123+**On server change** — reset `syncedAt = 0` to force full re-sync. No need to touch syncSource.
124124+125125+**Files to update:**
126126+- `backend/electron/sync.ts` — lines 393, 515-525, 619-620, 667-668, 773-775
127127+- `sync/sync.js` — lines 102-107, 162-166, 277-282, 399-400
128128+- `backend/tauri/src-tauri/src/sync.rs` — lines 521-522, 634-665, 799-800, 894
129129+130130+### 2. Stop setting syncSource on item creation
131131+132132+**Browser extension** — remove `syncSource` param from `addItem()` calls:
133133+- `backend/extension/history.js:119` — remove `syncSource: 'history'`
134134+- `backend/extension/tabs.js:138` — remove `syncSource: 'tab'`
135135+- `backend/extension/bookmarks.js:84` — remove `syncSource: 'bookmark'`
136136+137137+Classifications are already preserved in tags (`from:*`) and metadata JSON.
138138+139139+**Datastore** — remove syncSource gating on device metadata:
140140+- `backend/electron/datastore.ts:2322-2324` — always call `addDeviceMetadata()`, not just when `!options.syncSource`
141141+- `backend/electron/datastore.ts:2404-2406` — same for updates
142142+143143+### 3. Device ID prefix removal
144144+145145+Device IDs currently use platform prefixes: `"desktop-{uuid}"` (Electron), `"extension-{uuid}"` (browser extension). These prefixes are **not used in any conditional logic** — verified by codebase-wide search. No code checks `startsWith('desktop')` or parses the prefix.
146146+147147+**Change:** Generate plain UUIDs going forward. Platform/type info belongs in the devices table.
148148+149149+**Migration:** On startup, detect prefixed device IDs and strip the prefix:
150150+151151+```
152152+"desktop-550e8400-e29b-41d4-a716-446655440000" → "550e8400-e29b-41d4-a716-446655440000"
153153+```
154154+155155+This requires updating:
156156+1. The stored device ID in `extension_settings`
157157+2. All `metadata._sync.createdBy` and `metadata._sync.modifiedBy` references in existing items
158158+159159+**Detection logic:** Check if device ID matches `/{prefix}-[0-9a-f]{8}-/` pattern. Known prefixes: `desktop-`, `extension-`. Strip prefix, keep UUID.
160160+161161+**One exception:** `backend/extension/environment.js:29` checks `parsed.startsWith('extension-')` for the extension device ID. This needs updating to handle both old prefixed and new plain UUID formats during the transition period.
162162+163163+**Files to update:**
164164+- `backend/electron/device.ts:46` — `crypto.randomUUID()` (remove `desktop-` prefix)
165165+- `backend/extension/environment.js` — update `startsWith('extension-')` check
166166+- Add migration in `backend/electron/datastore.ts` (startup)
167167+- Add migration in `backend/extension/` (startup)
168168+169169+### 4. New `devices` table
170170+171171+Replace the ad-hoc `extension_settings` K/V storage with a proper table.
172172+173173+```sql
174174+CREATE TABLE IF NOT EXISTS devices (
175175+ id TEXT PRIMARY KEY, -- Plain UUID
176176+ name TEXT DEFAULT '', -- User-friendly: hostname, "iPhone", etc.
177177+ platform TEXT NOT NULL, -- 'electron', 'tauri', 'tauri-mobile', 'extension', 'server'
178178+ metadata TEXT DEFAULT '{}', -- JSON: OS, arch, app version, backend type, capabilities
179179+ createdAt INTEGER NOT NULL, -- First registration
180180+ lastSeenAt INTEGER NOT NULL -- Last activity (updated on sync, app launch, etc.)
181181+);
182182+```
183183+184184+**All clients register on startup.** Server nodes also register (even though they don't originate items today, they may in the future — e.g., server-side feed fetching).
185185+186186+**The `metadata._sync.createdBy` / `modifiedBy` fields** reference `devices.id` (plain UUID).
187187+188188+**Synced table:** The devices table itself should sync across all nodes so every client knows about every device. This enables UI like "saved from MacBook" or "last synced from iPhone".
189189+190190+**Schema location:** Add to `schema/v1.json` (or v2 if we version it).
191191+192192+### 5. Mobile filtering (separate task)
193193+194194+Once syncSource cleanup is done and device IDs are reliable, mobile can filter the default list view:
195195+196196+- Items with `metadata._sync.createdBy` matching a device where `platform = 'electron'` AND tagged `from:history` or created by navigation tracking → hide from default list, show in search
197197+- This is a frontend-only change on mobile (no schema changes needed)
198198+199199+## Migration & Backward Compatibility
200200+201201+### syncSource column
202202+203203+**Keep the column** in the schema — removing it would break older clients. Just stop reading/writing it in new code. It becomes a no-op legacy field.
204204+205205+### Existing items
206206+207207+No data migration needed for sync to work. The new push query (`syncedAt = 0 OR updatedAt > syncedAt`) handles all existing items correctly:
208208+- Items with `syncSource = ''` and `syncedAt = 0` → pushed (same as before)
209209+- Items with `syncSource = 'server'` and `syncedAt > 0` → only pushed if modified (same as before)
210210+- Items with `syncSource = 'history'` and `syncedAt = 0` → now pushed (new — these were previously blocked)
211211+212212+The last point means browser imports will start syncing after this change. This is the desired behavior ("everything syncs").
213213+214214+### Device ID migration
215215+216216+Run on startup before any sync operations:
217217+218218+1. Read current device ID from `extension_settings`
219219+2. If it matches `{prefix}-{uuid}` pattern, strip the prefix
220220+3. Update `extension_settings` with the plain UUID
221221+4. Scan items table: `UPDATE items SET metadata = replace(metadata, old_device_id, new_device_id) WHERE metadata LIKE '%' || old_device_id || '%'`
222222+5. Register in new `devices` table
223223+224224+### Rollback safety
225225+226226+If needed, the old sync algorithm still works — it just won't push browser imports (same as today). The syncSource column is preserved, so reverting the push query is a one-line change.
227227+228228+## Prod Impact
229229+230230+- **Low risk**: The sync algorithm change is additive (more items sync, none stop syncing)
231231+- **Browser imports will start syncing**: This is intentional but may surprise users who had large history imports. Consider a one-time notification or a toggle.
232232+- **No breaking API changes**: Server doesn't use syncSource for anything — it stores and returns it, that's all
233233+- **Device ID migration is local-only**: Each client migrates its own stored ID on startup. No server coordination needed.