Why sync stopped at 1,000 tasks
For accounts past 1,000 tasks, every change in weekkii synced and then reverted moments later. The culprit was a silent 1,000-row cap in our API layer.
On July 1 we fixed the strangest bug weekkii has shipped so far. If your account held more than 1,000 tasks, every change you made would apply, sync to the server, and then quietly un-happen a few moments later. Add a task, watch it vanish. Complete a task, watch it come back unchecked. One affected account, sitting at 1,012 tasks, looked genuinely haunted.
Here is what was actually going on.
weekkii is local-first. Every read hits a SQLite database on your device, and a sync loop pushes your changes up and pulls the server's state down. The pull merges server rows into local ones, with one important rule: local rows you have edited but not yet synced win over whatever the server sends. The merge also handles deletion detection, and that is where the trouble lived. To decide whether a task was deleted on another device, the merge compares the set of rows the server returned against what it has locally. Anything local, already synced, and missing from the server's response is treated as deleted elsewhere and pruned.
That logic is correct on exactly one condition: the pulled set has to be the complete set.
Our database API layer is PostgREST, the REST layer that sits in front of Supabase Postgres. PostgREST ships with a server-side default we had never hit: any query returns at most 1,000 rows. Silently. No error, no complaint, just a truncated result that looks exactly like a complete one. Our sync pull fetched the entire task table with one unbounded select. Under 1,000 tasks, that select really did return everything. Past 1,000, it returned only the 1,000 oldest rows, which means the rows missing from the response were precisely the newest ones: the tasks you had just added, completed, or edited. The merge looked at that truncated set, concluded your newest work had been deleted on the server, and deleted it locally too. Every mutation applied, synced successfully, and was then reverted by our own deletion detection on the next pull.
How we caught it
We do not run analytics SDKs or third-party error trackers; error and telemetry logs go to a table in our own Supabase project. Scanning that table for the affected account, one detail jumped out. Pull after pull logged fetched: 1000. Not 998, not 1,003. Exactly 1,000, every time, on an account holding 1,012 tasks. A row count that flat is never a coincidence. Once we saw the number, the rest was a small diff waiting to be written.
The fix is keyset pagination. The pull now fetches a page, remembers the last id it saw, asks for the next page starting after that id, and stops when a page comes back short. The merge always sees the whole table, so deletion detection is fed the truth again.
What we learned
Three things, honestly.
First, an API that silently truncates is a footgun. If PostgREST had thrown a hard error at row 1,001, we would have hit it in development the first time a test account crossed the line. A silent cap meant the failure only surfaced in production, only for our heaviest users, and only as spooky downstream behavior with no obvious connection to its cause.
Second, any code that diffs "what the server has" against "what I have" must be fed the whole server set. A deletion detector working from a partial snapshot does not degrade gracefully. It invents deletions. If we ever write merge logic like this again, the first thing we will verify is the completeness of the input, not the cleverness of the diff.
Third, the honest admission: paginating a full-table pull is still a full-table pull. It is now correct at any size, but it does work proportional to your entire history on every sync. The real fix on our roadmap is incremental sync, pulling only rows that changed since the last pull, so the cost scales with your edits instead of your archive.
The haunted account is just an account again: 1,012 tasks, every one of them staying exactly where it was put.