Learn Flashcards Cloud Sync

Syncs the CONTENT (id/type/front/back) of CardMirror's Learn spaced-repetition flashcards to a signed-in user's account, closing the same account-linking gap Quick Cards Cloud Save closed for the reusable-snippet library — the Learn store (packages/debate-editor/src/editor/learn-store.ts) was device-local only.

  • Package: debate-editor (learnCardsSync, LearnCardsSync, learn-cards-client), apps/debate-ai.com (/api/learn-cards, saved_learn_cards D1 table)

What it does

On boot, once the local Learn store has loaded, learnCardsSync.init() merges once against the account: any card synced from another device but missing locally is adopted under its own id (never duplicated), and any card that exists only on this device is best-effort pushed up — neither side ever overwrites the other, it only fills gaps. After the merge, every later card create, edit, or delete (including a file's cards being pruned via "forget this file") mirrors itself to the account the same way. Signed out, or under the Electron desktop build, the Learn store keeps working exactly as before, purely on-device.

What's intentionally excluded

Only a card's portable content syncs — its review schedule (due date, interval, lapses), file anchors, AI threads, and private notes all stay local-only. A card adopted on a new device starts with no schedule pressure, the same way the manage GUI's own JSON export/import already treats a card whose carried schedule is absent — restoring a card is not meant to restore a specific device's review history. Custom decks and the review log each sync separately — see Learn Decks Cloud Sync and Learn Review Log Cloud Sync. The remaining sub-collections (anchors, AI threads, notes, doc registry) live in one shared blob per device (not one row per record), so they don't fit this sync's per-record shape; syncing them would need a bespoke schema of their own.

Every per-record push carries optimistic concurrency: PUT /api/learn-cards/[cardId] compares the incoming card's own updatedAt against whatever's already saved for that id, and rejects (409) instead of overwriting when the saved version is newer — an edit made on another device that hasn't reached this browser yet. On that rejection, this browser adopts the newer version into its own LearnStore instead of losing it, the same "never overwrite, only reconcile" rule init()'s merge already follows — mirroring Quick Cards Cloud Save's own fix for the identical race. CardDef didn't carry an updatedAt field before this: it's now stamped by upsertCard/importCards themselves (not by callers), and stays optional on the type so a card written before this field existed — locally, or already synced to the account — still round-trips; a side with no updatedAt is treated as arbitrarily old rather than blocking the sync, which is exactly the previous last-write-wins behavior until both sides have a real timestamp.

Known gaps

None open.