Schema: fields the client needs to sync watch state / watchlist / prefs #1
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/sync-fields"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Backend half of richiexec/myanime-app#11 (cloud sync). The #9 schema sketched these collections before the Flutter client existed; wiring up the client turned up three gaps that block the acceptance criteria.
What changed
meta(json) onwatch_stateandwatchlist. The client's local rows carry display fields — episode, title, cover art, MAL id, completed/dismissed — with nowhere to live server-side. Without them a freshly-signed-in device pulls resume points it can't render (untitled, art-less tiles). Opaque to the server.updatedAt/addedAtwereautodate, i.e. stamped on server receipt. That breaks last-write-wins: a device that edits offline Monday and pushes Friday would beat a device that edited Tuesday and synced at once. They're now client-setdatefields (the time the user acted). The old server autodate lives on asupdatedand becomes the pull cursor — that one must stay server-side, or a device with a skewed clock could write rows behind another device's cursor and stay invisible to it forever.deletedAttombstone onwatchlist. A hard delete is an absence, and an absence isn't pullable — the next device to sync just re-adds the title from its own copy. Soft-delete makes a removal a state other devices can pull; re-adding clears it on the same row (the unique(profile,itemId)index guarantees no duplicate).Migration safety
The type change means dropping and re-adding the timestamp fields, which drops their column data. Live is believed empty, but blanking
updatedwould leave any existing row invisible to every future pull — too quiet a failure to risk on an assumption — so the migration snapshots the old timestamps and writes them back.Verification
scripts/verify.pygrows the new field-shape assertions plus behavioural checks: the client clock round-trips un-restamped, the server stamps its own cursor, a duplicate(profile,itemId)is rejected, and delete → re-add reuses the row instead of duplicating. Ran against a localdocker compose up --buildon both paths:Deploy note: redeploy the Coolify PocketBase after merge so the live schema picks this up before the client PR ships.
🤖 Generated with Claude Code
Review: approve ✅ — safe to deploy
Reviewed from a local checkout of the branch. The migration is correct and the design is right.
Two clocks, correctly assigned. Client
updatedAt/addedAtfor last-write-wins (the time the user acted, so an offline edit pushed late doesn't beat a later edit synced promptly), serverupdatedautodate as the pull cursor (monotonic in server time, so a skewed device clock can't stamp a row behind another device's cursor and vanish). This is the correct resolution of a subtle problem, and the client PR (#27 in the app repo) implements exactly the matching half.Verified the one load-bearing assumption: the migration only adds
updatedAttoprefsand relies onprefs.updatedalready existing as the cursor — and it does, from the init schema (1752600000_init_accounts_schema.js:140). So prefs ships with a working cursor. ✅The autodate→date swap is handled safely. Type is immutable in PocketBase, so drop-and-re-add is necessary;
snapshot/restore(raw SQL, so autodate hooks don't restamp) preserves existing timestamps and seeds both clocks. On the live backend it's a no-op anyway — these collections are empty (no client has synced yet), so there's nothing to preserve. Safe to deploy.verify.py earns its keep — it writes client-shaped rows (
itemId:"movie:278:0", clientupdatedAt,meta, tombstone) and asserts the client clock round-trips un-restamped, the duplicate(profile,itemId)is rejected, and delete→re-add reuses the one row.Minor notes (non-blocking)
prefs.updatedAtbut notprefs.updated— I confirmed the latter by hand; a one-line assert would close the gap.app.save()calls. Near-impossible against empty collections, but if the deploy errors mid-migration, inspect collection state before re-deploying rather than blindly retrying — a half-applied run could trip a duplicate-field error on re-run.Deploy this, then merge app PR #27 — the client writes
updatedAt/meta/deletedAt, which the live schema won't have until this lands, and the failure is silent (see my review on #27).