mitbringsl/docs/SYNC.md
Tronax cdc0c785b9
Docs: catch up to auth discovery, settings screen and sync fixes
Document the four features that landed on main in b44bc8c..85c790e:

- AGENTS.md: post-MVP section now covers server-driven auth discovery
  (GET /api/config, AUTH_PASSWORD_ENABLED 403 enforcement,
  OIDC_GENERIC_DISPLAY_NAME), the settings screen with GET/PUT /api/me,
  and the critical Android sync/data-safety fixes (pull-all-lists,
  client HLC tick per server op, no destructive migration, ProGuard
  rules). Repo structure updated (httpapi config.go/me.go, ui/settings),
  roadmap entries added, open-points list extended with test backlog for
  the new endpoints.
- API.md: new sections for GET /api/config (public) and GET/PUT /api/me;
  403 responses documented for register/login when password auth is off.
- SYNC.md: client pull loop (every tracked list, op_log pruning) and the
  client HLC discipline (tick per incoming server op) that keeps LWW
  correct across devices with skewed clocks.
- README: highlights for auth discovery/OIDC-only mode and the settings
  screen.
2026-08-22 09:45:45 +02:00

90 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mitbringsl Synchronization Protocol & Conflict Resolution
## 1. Synchronization Model
Mitbringsl uses an **Append-Only Operation Log (`op_log`)** with **Hybrid Logical Clock (HLC)** timestamps and **Last-Write-Wins (LWW)** projections.
---
## 2. Hybrid Logical Clock (HLC)
Every operation carries a 64-bit integer timestamp `hlc_ts`:
$$\text{hlc\_ts} = (\text{wall\_ms} \ll 16) \mid \text{counter}$$
- **`wall_ms`** (48 bits): Unix epoch milliseconds.
- **`counter`** (16 bits): Logical counter disambiguating events produced within the same millisecond.
### Guarantees
1. **Strict Monotonicity**: $t_{n+1} > t_n$ for all events generated on the same device.
2. **Causal Ordering**: If event $B$ was generated after observing event $A$, then $HLC(B) > HLC(A)$.
---
## 3. Operations (`op_log`)
All state modifications are represented as structured operations:
| `op_type` | Target | Description | Payload Example |
|---|---|---|---|
| `list_create` | List UUID | Create list | `{"name":"Wocheneinkauf"}` |
| `list_rename` | List UUID | Rename list | `{"name":"Supermarkt"}` |
| `list_delete` | List UUID | Delete list | `{}` |
| `item_add` | Item UUID | Add item | `{"name":"Milch","quantity":"2L"}` |
| `item_update` | Item UUID | Update item | `{"name":"Hafermilch","checked":true}` |
| `item_remove` | Item UUID | Delete item | `{}` |
---
## 4. Conflict Resolution (LWW & Tombstones)
1. **Projection Updates**:
Database tables (`items`, `lists`) are projections of `op_log`. When an op arrives:
```sql
UPDATE items
SET name = $1, hlc_ts = $2, updated_at = now()
WHERE id = $3 AND hlc_ts < $2 AND deleted_at IS NULL;
```
2. **Tombstones (`deleted_at`)**:
When an item or list is deleted (`item_remove`, `list_delete`), a tombstone is set (`deleted_at = now()`). Late-arriving offline `item_update` operations with smaller `hlc_ts` values will **never** revive a tombstoned entity.
---
## 5. Idempotent Push & Cursor Pull
- **Idempotent Push (`POST /api/lists/{id}/ops`)**:
Each operation carries a `(client_id, client_seq)` tuple enforced by a `UNIQUE` constraint in Postgres (`op_log`). Re-sent requests return previous `(seq, hlc_ts)` assignments without duplicating side effects.
- **Cursor Pull (`GET /api/lists/{id}/ops?since={seq}`)**:
Clients track `max(server_seq)` locally. Incremental sync fetches ops where `seq > cursor`, sorted by monotonic server sequence `seq ASC`.
- **Client pull loop**:
The `SyncWorker` pulls for **every** tracked list on each run (not only lists with pending outbox ops), so remote edits on "quiet" (e.g. shared/joined) lists arrive reliably. Old synced `op_log` rows are pruned locally to bound growth.
### Client HLC discipline
On every incoming server op the client advances its local HLC with
`tick(op.hlc_ts)`. This is essential for LWW correctness across devices with
skewed clocks: without it, a fast-clock device would permanently win conflicts
while a slow-clock device's own edits would be silently rejected by the
`hlc_ts <` projection guard. The same rule applies on the server (`opstore`
ticks the server HLC per incoming op).
---
## 6. Shared Lists
Since shared lists were introduced, ops flow between **all members** of a list,
not just its owner:
- **Membership**: A list has an owner (`lists.owner_id`, role `owner`) and any
number of members (`list_members`, role `member`). Members join via an
8-character invite code (`POST /api/lists/join`); owner and members can read
the code (`POST /api/lists/{id}/invite`) to share it.
- **Sync scope**: Push and Pull verify on every request that the caller is owner
or member of the list — non-members receive `404` (not `403`, so membership
cannot be probed). Once joined, a member pulls the full op history
(`?since=0`) and applies the same LWW projection locally.
- **Conflict semantics are unchanged**: With multiple writers the LWW register
`(hlc_ts, client_id)` resolves concurrent edits; the HLC guarantees that ops
from different clients that observed each other are still causally ordered.
Concurrent edits to the same item converge to the highest `hlc_ts` on every
device last write wins, no data merge.