Offline sync
PennyPlan must work with no signal. The web app saves every entry on the device first, then sends the queue
(the outbox) to POST /v1/entries/sync whenever it can. This page is the contract between the two.
The one rule: never lose an entry
An entry sitting in the outbox is the person’s data. It is removed only after the server has said it has it. Not on an error, not on a sign-out, not on an expired session, not on an app update.
How an entry travels
- The device creates the entry with a client-generated UUID and saves it locally. This works fully offline.
- When online, it sends the outbox, oldest first, up to 200 entries per call.
- The server handles every entry independently and returns one result per entry, in the same order.
- For each result the device acts as below, and only then removes it from the outbox.
| Result | Meaning | The device should |
|---|---|---|
created | The server saved it | Remove from the outbox |
updated | The server applied your newer edit | Remove from the outbox |
unchanged | The server already had exactly this | Remove from the outbox |
stale | The server already holds a newer edit | Replace the local copy with the entry in the result, then remove from the outbox |
rejected | This entry is invalid (see error.details) | Keep it, mark it as needing attention, and show the problem. Never drop it silently |
Because every entry has its own result, one bad entry never blocks the others.
Retrying is always safe
Sending the same batch again changes nothing: the second time every entry comes back unchanged. So if the
connection drops halfway and you do not know what arrived, just send it all again. This is the reason ids are
generated on the device.
Last write wins, by the device’s clock for that edit
Each entry carries clientUpdatedAt, the moment it was last edited on the device. If two devices edit the
same entry, the later clientUpdatedAt wins. An older edit arriving late comes back as stale and is ignored, so
the device learns what the server kept.
Deleting offline
A deletion is sent as the full entry with "deleted": true and its own, newer clientUpdatedAt. The server keeps
a hidden record of it, so:
- an older edit that arrives late cannot bring the entry back (
stale); - a newer edit can, if the person really did re-create it.
When the session has expired
The device may be offline for days and its sign-in may have expired in the meantime. The login is a cookie managed by the web app’s server (see ADR 0005), so the outbox cannot send anything until the person signs in again.
When a sync call comes back 401:
- Stop syncing and keep the outbox untouched. Do not retry in a loop.
- Show a clear, calm message: “Your entries are saved on this phone. Sign in to send them.”
- After the person signs in, resume from the same outbox. Nothing is lost or re-entered.
- If signing in produces a different account than the one that created the entries, do not send them under the new account. Ask the person what to do.
Limits
| Limit | Value |
|---|---|
| Entries per sync call | 200 |
| Amount | Whole kobo, 1 to 2,000,000,000 |
| Note | 500 characters |
| Counterparty | 120 characters |
| Date | Not before 2000, not more than a day ahead |
How it is tested
The API’s real-database tests send a 60-entry offline session twice and check there are exactly 60 entries; sync the same new entry four times at once and check there is exactly one; confirm a late old edit cannot resurrect a deleted entry; and confirm that one user can never touch another’s entries by reusing an id.