···30303131**Sync & Conflict Resolution:**
32323333-- [ ] Bi-directional sync: local drafts → PDS records, PDS records → local cache
3434-- [ ] Conflict resolution strategy for concurrent edits (last-write-wins or merge UI)
3333+- [x] Bi-directional sync infrastructure
3434+- [x] Conflict resolution strategy
3535+- [ ] API endpoints for sync operations
3536- [ ] Offline queue for pending publishes
3737+ - [ ] Frontend sync store with IndexedDB persistence
3638- [ ] Sync status UI indicators
37393840**Deep Linking:**
+83
migrations/015_2026_01_03_sync_tracking.sql
···11+-- Sync tracking infrastructure for bi-directional PDS synchronization
22+-- Adds version tracking and sync status to core tables
33+44+-- Sync status enum (idempotent creation)
55+DO $$ BEGIN
66+ CREATE TYPE sync_status AS ENUM (
77+ 'local_only', -- Never synced to PDS
88+ 'synced', -- In sync with PDS
99+ 'pending_push', -- Local changes need to be pushed
1010+ 'conflict' -- Local and remote both changed
1111+ );
1212+EXCEPTION
1313+ WHEN duplicate_object THEN null;
1414+END $$;
1515+1616+ALTER TABLE decks
1717+ ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1,
1818+ ADD COLUMN IF NOT EXISTS pds_cid TEXT,
1919+ ADD COLUMN IF NOT EXISTS pds_uri TEXT,
2020+ ADD COLUMN IF NOT EXISTS sync_status sync_status NOT NULL DEFAULT 'local_only',
2121+ ADD COLUMN IF NOT EXISTS last_synced_at TIMESTAMPTZ;
2222+2323+ALTER TABLE cards
2424+ ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1,
2525+ ADD COLUMN IF NOT EXISTS pds_cid TEXT,
2626+ ADD COLUMN IF NOT EXISTS pds_uri TEXT,
2727+ ADD COLUMN IF NOT EXISTS sync_status sync_status NOT NULL DEFAULT 'local_only',
2828+ ADD COLUMN IF NOT EXISTS last_synced_at TIMESTAMPTZ;
2929+3030+ALTER TABLE notes
3131+ ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1,
3232+ ADD COLUMN IF NOT EXISTS pds_cid TEXT,
3333+ ADD COLUMN IF NOT EXISTS pds_uri TEXT,
3434+ ADD COLUMN IF NOT EXISTS sync_status sync_status NOT NULL DEFAULT 'local_only',
3535+ ADD COLUMN IF NOT EXISTS last_synced_at TIMESTAMPTZ;
3636+3737+CREATE TABLE IF NOT EXISTS sync_log (
3838+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3939+ owner_did TEXT NOT NULL,
4040+ entity_type TEXT NOT NULL, -- 'deck', 'card', 'note'
4141+ entity_id UUID NOT NULL,
4242+ operation TEXT NOT NULL, -- 'push', 'pull', 'conflict_resolve'
4343+ status TEXT NOT NULL, -- 'pending', 'success', 'failed'
4444+ pds_cid TEXT,
4545+ error_message TEXT,
4646+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4747+ completed_at TIMESTAMPTZ
4848+);
4949+5050+CREATE INDEX IF NOT EXISTS idx_sync_log_owner_did ON sync_log(owner_did);
5151+CREATE INDEX IF NOT EXISTS idx_sync_log_entity ON sync_log(entity_type, entity_id);
5252+CREATE INDEX IF NOT EXISTS idx_sync_log_status ON sync_log(status);
5353+CREATE INDEX IF NOT EXISTS idx_sync_log_created_at ON sync_log(created_at DESC);
5454+5555+CREATE INDEX IF NOT EXISTS idx_decks_sync_status ON decks(sync_status) WHERE sync_status != 'synced';
5656+CREATE INDEX IF NOT EXISTS idx_cards_sync_status ON cards(sync_status) WHERE sync_status != 'synced';
5757+CREATE INDEX IF NOT EXISTS idx_notes_sync_status ON notes(sync_status) WHERE sync_status != 'synced';
5858+5959+CREATE OR REPLACE FUNCTION increment_version_on_update()
6060+RETURNS TRIGGER AS $$
6161+BEGIN
6262+ -- Only increment if content changed (not just sync metadata)
6363+ IF (TG_TABLE_NAME = 'decks' AND (NEW.title != OLD.title OR NEW.description != OLD.description OR NEW.tags != OLD.tags)) OR
6464+ (TG_TABLE_NAME = 'cards' AND (NEW.front != OLD.front OR NEW.back != OLD.back OR NEW.media_url IS DISTINCT FROM OLD.media_url)) OR
6565+ (TG_TABLE_NAME = 'notes' AND (NEW.title != OLD.title OR NEW.body != OLD.body OR NEW.tags != OLD.tags)) THEN
6666+ NEW.version = OLD.version + 1;
6767+ -- Mark as pending push if it was synced
6868+ IF OLD.sync_status = 'synced' THEN
6969+ NEW.sync_status = 'pending_push';
7070+ END IF;
7171+ END IF;
7272+ RETURN NEW;
7373+END;
7474+$$ LANGUAGE plpgsql;
7575+7676+CREATE TRIGGER increment_decks_version BEFORE UPDATE ON decks
7777+ FOR EACH ROW EXECUTE FUNCTION increment_version_on_update();
7878+7979+CREATE TRIGGER increment_cards_version BEFORE UPDATE ON cards
8080+ FOR EACH ROW EXECUTE FUNCTION increment_version_on_update();
8181+8282+CREATE TRIGGER increment_notes_version BEFORE UPDATE ON notes
8383+ FOR EACH ROW EXECUTE FUNCTION increment_version_on_update();