docs: ownership model and ownerId in API contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 08:28:44 +00:00
parent f8955be4e9
commit bafdb88f5d

View File

@@ -29,16 +29,22 @@ the API, on shared PostgreSQL, behind Zitadel auth, deployed via Coolify.
## API ## API
Every `/api/**` route requires a valid Zitadel **access token** (`Authorization: Bearer …`) Every `/api/**` route requires a valid Zitadel **access token** (`Authorization: Bearer …`)
belonging to the owner. Missing/invalid/expired → `401`. No anonymous access. carrying the `user` project role. Missing/invalid/expired/role-less`401`. No anonymous access.
**Ownership:** every row carries `owner_id` = the writer's token `sub`. All reads and writes are
scoped server-side to the caller (`owner_id = sub OR owner_id IS NULL`); full-replace endpoints
only replace the caller's partition. `owner_id IS NULL` marks legacy pre-multi-user rows — visible
to all authorized users and adopted by the next write that touches them. DTOs expose this as a
nullable `ownerId`; any client-supplied `ownerId` is ignored.
| Method & path | Caller | Behaviour | | Method & path | Caller | Behaviour |
|---|---|---| |---|---|---|
| `PUT /api/lists` | desktop | Body `[{id,name}]` = full catalog. Upserts all, deletes lists not in payload (cascades tasks). → 200 | | `PUT /api/lists` | desktop | Body `[{id,name}]` = full catalog. Upserts all, deletes lists not in payload (cascades tasks). → 200 |
| `GET /api/lists` | web | → 200 `[{id,name}]` | | `GET /api/lists` | web | → 200 `[{id,name,ownerId}]` |
| `GET /api/lists/{id}/tasks` | web | → 200 tasks for the list; 404 if unknown | | `GET /api/lists/{id}/tasks` | web | → 200 tasks for the list; 404 if unknown |
| `POST /api/tasks` | web | Body `{title, description?, listId}`. Server-generated GUID, `source=web`, `consumed=false`. 404 if listId unknown. → 201 with the created task | | `POST /api/tasks` | web | Body `{title, description?, listId}`. Server-generated GUID, `source=web`, `consumed=false`. 404 if listId unknown. → 201 with the created task |
| `PUT /api/tasks/mirror` | desktop | Body `[{id, listId, title, description?}, ...]` = the FULL current Idle backlog (camelCase; `[]` is valid). Full-replace of the desktop-owned partition: upsert each (as `consumed=true`), delete any `consumed=true` task not in the array, leave web-created `consumed=false` tasks untouched. Mirrors `PUT /lists`. → 200 | | `PUT /api/tasks/mirror` | desktop | Body `[{id, listId, title, description?}, ...]` = the FULL current Idle backlog (camelCase; `[]` is valid). Full-replace of the desktop-owned partition: upsert each (as `consumed=true`), delete any `consumed=true` task not in the array, leave web-created `consumed=false` tasks untouched. Mirrors `PUT /lists`. → 200 |
| `GET /api/tasks?consumed=false` | desktop | → 200 `[{id, listId, title, description, createdAt}]` web tasks not yet imported (awaiting pull) | | `GET /api/tasks?consumed=false` | desktop | → 200 `[{id, listId, title, description, ownerId, createdAt}]` web tasks not yet imported (awaiting pull) |
| `POST /api/tasks/{id}/consume` | desktop | Sets `consumed=true` (imports a pulled web task into the desktop partition). Idempotent. → 200; 404 if unknown | | `POST /api/tasks/{id}/consume` | desktop | Sets `consumed=true` (imports a pulled web task into the desktop partition). Idempotent. → 200; 404 if unknown |
| `PUT /api/tasks/{id}` · `DELETE /api/tasks/{id}` | desktop | Legacy per-task upsert/delete — **superseded by `PUT /tasks/mirror`**. Kept for compatibility. | | `PUT /api/tasks/{id}` · `DELETE /api/tasks/{id}` | desktop | Legacy per-task upsert/delete — **superseded by `PUT /tasks/mirror`**. Kept for compatibility. |