Files
ClaudeDo/docs/online-inbox-api-contract.md
2026-06-10 09:35:20 +02:00

6.2 KiB

ClaudeDo Online Inbox — API Contract & VPS build prompt

Status: handoff doc. The server side (API + minimal web client) is built and deployed VPS-side by a separate Claude instance. This file is the source of truth for the contract both ends implement against. The desktop client in this repo is built to match it.


1. Concept

ClaudeDo is a local desktop app that runs tasks autonomously via the Claude CLI; it is normally fully local (SQLite). The Online Inbox is an optional service that lets the single owner view their task lists and add new tasks from a phone/browser. The desktop app syncs against it.

Governing rule: the online store mirrors EXACTLY the desktop's Idle backlog — nothing else. A task is present online only while it is Idle on the desktop. The moment the user queues it locally, the desktop removes it from the online store. Running / WaitingForReview / Done / Failed / Cancelled tasks never appear online.

Sync directions (each one-way per entity → no conflict resolution needed):

  • Lists: desktop → online only. Desktop is the source of truth (full-replace catalog).
  • Idle tasks: desktop mirrors its Idle backlog up; the web can create new ones, which the desktop pulls down and then owns.

Single user. Both the desktop and the web client authenticate as the same Zitadel user.

2. Idle backlog definition (desktop side)

The desktop mirrors only "real" backlog items, not planning internals:

  • Status == Idle
  • ParentTaskId == null (no planning/improvement children)
  • PlanningPhase == None
  • BlockedByTaskId == null

3. Data model (Postgres)

lists
  id          text primary key            -- GUID supplied by the desktop; reuse verbatim
  name        text not null
  updated_at  timestamptz not null default now()

tasks
  id          text primary key            -- GUID; SHARED id space (see below)
  list_id     text not null references lists(id) on delete cascade
  title       text not null
  description text
  imported    boolean not null default false  -- false = web-created, awaiting desktop pull
                                                -- true  = desktop-owned (mirrored or handed off)
  created_at  timestamptz not null default now()
  updated_at  timestamptz not null default now()

Shared GUID id space. Web-created tasks get a server-generated GUID; the desktop imports under that SAME id, so it never duplicates. Desktop-mirrored tasks arrive with their own GUID. All task writes are idempotent upserts keyed on id.

imported flag = ownership.

  • Web POST /tasks inserts imported=false.
  • Desktop pulls imported=false, creates the task locally (reusing the id), then POST /tasks/{id}/imported flips it to true. From then on the task belongs to the desktop mirror.
  • PUT /tasks/mirror only ever inserts/updates/deletes within the imported=true partition. It never touches imported=false rows (those are pending handoff).

4. Endpoints

All endpoints require a valid Zitadel access token (Authorization: Bearer <token>). Missing/invalid/expired → 401. No anonymous access (imported tasks can trigger code execution on the user's machine).

Method & path Caller Body Response
PUT /lists desktop [{ "id", "name" }] — the FULL catalog 200
GET /lists web 200 [{ "id", "name" }]
GET /lists/{id}/tasks web 200 tasks in that list (404 if list unknown)
POST /tasks web { "title", "description"?, "listId" } 201 created task incl. id
GET /tasks?imported=false desktop 200 [{ "id","listId","title","description","createdAt" }]
POST /tasks/{id}/imported desktop 200 (404 if unknown)
PUT /tasks/mirror desktop [{ "id","listId","title","description" }] — full Idle set 200

Semantics:

  • PUT /lists — full replace: upsert all supplied, DELETE any list not in the payload (cascades its tasks). Idempotent.
  • POST /taskslistId must exist (400/404 otherwise). Server generates the id.
  • PUT /tasks/mirror — full replace of the imported=true partition: upsert every task in the payload (insert with imported=true, or update), and DELETE any imported=true task whose id is not in the payload. imported=false rows are untouched. Idempotent.
  • All task ids are client-trusted within the shared space; the server never rewrites an id.

5. Reconcile loop (desktop, runs each poll cycle)

1. PULL:  GET /tasks?imported=false
          for each: if no local task with that id → create local TaskEntity
                    { Id = remote.id, ListId = remote.listId, Title, Description,
                      Status = Idle, CreatedBy = "online" }
                    (skip + log if remote.listId has no local list)
                    then POST /tasks/{id}/imported
2. PUSH LISTS:  PUT /lists  with the full local catalog [{id, name}]
3. PUSH TASKS:  PUT /tasks/mirror  with the current local Idle backlog set (§2)

Ordering matters: pull+import+flag first, so the just-imported tasks are part of the local Idle set computed in step 3 and survive the mirror replace.

6. Minimal web client

Integrate into the existing Nuxt app at claudedo.kuns.dev if present; else a minimal page.

  • Zitadel login.
  • Show lists (GET /lists); select one to see its Idle tasks (GET /lists/{id}/tasks).
  • Add-task form → POST /tasks.
  • Mobile-first (main use: jotting ideas from a phone).
  • Create + read only. No editing, reordering, status changes, or deletes.

7. Security

  • Every route auth-gated (401 on bad token); only static assets / login are public.
  • Validate listId on task creation; parameterized queries only.
  • CORS restricted to the web client origin.
  • Don't log task titles/descriptions at info level (user content).

8. Deliverables from the VPS build

Report back so the desktop can be configured:

  1. API base URL.
  2. Zitadel app/client config the desktop must use: issuer/authority, client id, scopes, and the OAuth flow to use for a desktop app (device-code or auth-code + PKCE), plus how refresh tokens are issued.
  3. Any env vars / README.

Out of scope server-side: task execution (the desktop runs Claude), any task state other than the Idle mirror, multi-user / sharing / notifications.