docs: online inbox design spec
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
.nuxt/
|
||||
.output/
|
||||
.data/
|
||||
dist/
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
145
docs/superpowers/specs/2026-06-10-online-inbox-design.md
Normal file
145
docs/superpowers/specs/2026-06-10-online-inbox-design.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# ClaudeDo Online Inbox — Design
|
||||
|
||||
**Date:** 2026-06-10
|
||||
**Status:** Approved
|
||||
**Repo:** `claudedo-online` → deployed at `https://claudedo.kuns.dev`
|
||||
|
||||
## Purpose
|
||||
|
||||
Optional online mirror of the ClaudeDo desktop app's **Idle** task backlog, so the single
|
||||
owner can view lists and jot new tasks from a phone/browser. The desktop (.NET, SQLite,
|
||||
local) is the source of truth; this service stores/serves data only — it never executes
|
||||
tasks.
|
||||
|
||||
**Governing rule:** the online store mirrors EXACTLY the desktop's Idle tasks. A task is
|
||||
present online only while Idle. Queuing it on the desktop → desktop DELETEs it here.
|
||||
Running/Done/Failed/review tasks never appear online.
|
||||
|
||||
Two synced entities:
|
||||
- **Lists** — desktop → online only (full-replace catalog; desktop is source of truth).
|
||||
- **Idle tasks** — two-way creation. Desktop pushes its Idle tasks up (PUT); the web creates
|
||||
new ones (POST, `consumed=false`); the desktop pulls unconsumed, imports them, then POSTs
|
||||
consume.
|
||||
|
||||
## Stack & Architecture
|
||||
|
||||
Single Nuxt 3 deployable (TypeScript, Bun), `ssr: false` (SPA mode) so the
|
||||
`@kuns/zitadel-auth/vue` browser-OIDC package works directly. Built with `nuxt build`
|
||||
(node-server output) — **not** `nuxt generate` — so Nitro still serves the API.
|
||||
|
||||
| Layer | Implementation |
|
||||
|---|---|
|
||||
| Web client | Vue 3 SPA pages, mobile-first; login via `@kuns/zitadel-auth/vue` |
|
||||
| API | Nitro server routes under `/api/**` |
|
||||
| DB access | `postgres` (postgres.js), tagged-template parameterized queries only |
|
||||
| Auth | `jose` JWKS validation of Zitadel access tokens (mirrors preis-tracker) |
|
||||
| Deploy | Coolify → Docker (Bun) → Traefik; domain `claudedo.kuns.dev` |
|
||||
|
||||
Web + API share one origin, so the web client makes same-origin `/api` calls. The desktop is
|
||||
a non-browser HTTP client (CORS not enforced on it). CORS is still configured to allow only
|
||||
the configured web origin, per spec.
|
||||
|
||||
**Desktop API base URL:** `https://claudedo.kuns.dev/api`
|
||||
|
||||
## Data model (Postgres DB `claudedo`, shared PG16 instance)
|
||||
|
||||
Migration `server/db/migrations/0001_init.sql`, applied idempotently at container start.
|
||||
|
||||
```sql
|
||||
create table if not exists lists (
|
||||
id text primary key, -- GUID supplied by desktop, reused verbatim
|
||||
name text not null,
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists tasks (
|
||||
id text primary key, -- GUID, SHARED id space
|
||||
list_id text not null references lists(id) on delete cascade,
|
||||
title text not null,
|
||||
description text,
|
||||
source text not null, -- 'web' | 'desktop'
|
||||
consumed boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_tasks_list_id on tasks(list_id);
|
||||
create index if not exists idx_tasks_consumed on tasks(consumed) where consumed = false;
|
||||
```
|
||||
|
||||
- **Shared GUID id space.** Web-created tasks get a server-generated GUID; the desktop
|
||||
imports under that SAME id (never duplicates). Desktop tasks arrive with their own GUID via
|
||||
PUT. All task writes are idempotent upserts keyed on `id`.
|
||||
- `consumed` is the web→desktop handoff flag.
|
||||
|
||||
## Endpoints (exact contract, namespaced under `/api`)
|
||||
|
||||
| Method & path | Caller | Nitro file | Behaviour |
|
||||
|---|---|---|---|
|
||||
| `PUT /api/lists` | desktop | `server/api/lists.put.ts` | Body `[{id,name}]` = full catalog. Upsert all; DELETE lists not in payload (cascades tasks). Idempotent. → 200 |
|
||||
| `GET /api/lists` | web | `server/api/lists.get.ts` | → 200 `[{id,name}]` |
|
||||
| `GET /api/lists/:id/tasks` | web | `server/api/lists/[id]/tasks.get.ts` | → 200 tasks for list; 404 if list unknown |
|
||||
| `POST /api/tasks` | web | `server/api/tasks.post.ts` | Body `{title, description?, listId}`. Insert server GUID, `source='web'`, `consumed=false`. listId must exist (404). → 201 with created task incl. id |
|
||||
| `PUT /api/tasks/:id` | desktop | `server/api/tasks/[id].put.ts` | Body `{listId,title,description?}`. Idempotent upsert (`source='desktop'` on insert). → 200 (existing) / 201 (new) |
|
||||
| `DELETE /api/tasks/:id` | desktop | `server/api/tasks/[id].delete.ts` | Idempotent. → 204 (even if absent) |
|
||||
| `GET /api/tasks?consumed=false` | desktop | `server/api/tasks.get.ts` | → 200 `[{id,listId,title,description,createdAt}]` web-created, not yet imported |
|
||||
| `POST /api/tasks/:id/consume` | desktop | `server/api/tasks/[id]/consume.post.ts` | Set `consumed=true`. Idempotent. → 200; 404 if unknown |
|
||||
|
||||
Validation: `listId` existence checked on POST/PUT task; bad/missing body fields → 400.
|
||||
|
||||
## Auth
|
||||
|
||||
`server/middleware/auth.ts` gates **only** `/api/**` (static SPA shell + JS assets are public;
|
||||
the SPA performs the login redirect client-side).
|
||||
|
||||
- Require `Authorization: Bearer <access_token>`. Verify via `jose`
|
||||
`createRemoteJWKSet(new URL('${ZITADEL_ISSUER}/oauth/v2/keys'))` with `jwtVerify`,
|
||||
checking issuer + expiry + `aud` (must include one of the configured audiences: the web
|
||||
client id, desktop client id, or project id).
|
||||
- Enforce single owner: token `sub` ∈ `ALLOWED_USER_IDS`. Otherwise 401/403.
|
||||
- Missing/invalid/expired token → **401**. No anonymous access.
|
||||
- Do not log task titles/descriptions at info level (user content).
|
||||
|
||||
## Web UI (mobile-first; create + read ONLY)
|
||||
|
||||
1. Login (`@kuns/zitadel-auth/vue`, redirect to Zitadel hosted login).
|
||||
2. List of lists (`GET /api/lists`).
|
||||
3. Tap a list → its Idle tasks (`GET /api/lists/:id/tasks`).
|
||||
4. "Add task" form: title + optional description → `POST /api/tasks` to the selected list.
|
||||
|
||||
No editing, reordering, status changes, or deletes.
|
||||
|
||||
## Zitadel provisioning (via `ZITADEL_SERVICE_TOKEN` Management API)
|
||||
|
||||
Project **ClaudeDo** with two PKCE apps (both issue JWT access tokens; same owner user):
|
||||
|
||||
| App | Type | Grant | Redirect |
|
||||
|---|---|---|---|
|
||||
| ClaudeDo Web | User-Agent (SPA) | Auth Code + PKCE | `https://claudedo.kuns.dev/auth/callback`, post-logout `https://claudedo.kuns.dev` |
|
||||
| ClaudeDo Desktop | Native | Auth Code + PKCE + **offline_access** (refresh token, headless) | loopback `http://localhost:<port>/callback` |
|
||||
|
||||
API validates `aud` against both client IDs + the project ID (project-audience scope
|
||||
`urn:zitadel:iam:org:project:id:{projectId}:aud` requested by clients).
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Var | Used by | Notes |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` | API | `postgres://mika:…@l8kogcggsc80sgcgk8kswww4:5432/claudedo` |
|
||||
| `ZITADEL_ISSUER` | API + web | `https://auth.kuns.dev` |
|
||||
| `ZITADEL_AUDIENCE` | API | comma list: web id, desktop id, project id |
|
||||
| `ALLOWED_USER_IDS` | API | owner `sub` allowlist |
|
||||
| `WEB_ORIGIN` | API | CORS allowed origin = `https://claudedo.kuns.dev` |
|
||||
| `NUXT_PUBLIC_ZITADEL_CLIENT_ID` | web | ClaudeDo Web client id |
|
||||
| `NUXT_PUBLIC_ZITADEL_ISSUER` | web | `https://auth.kuns.dev` |
|
||||
|
||||
## Deliverables
|
||||
|
||||
API + migration, web client, Coolify deploy, README, and a report covering: (1) API base
|
||||
URL, (2) Zitadel config the desktop must use (issuer, client id, scopes incl. `offline_access`,
|
||||
redirect/refresh setup), (3) env vars.
|
||||
|
||||
## Out of scope
|
||||
|
||||
No task execution (desktop runs Claude). No task states beyond the Idle mirror. No
|
||||
multi-user, sharing, or notifications. Web client is create+read only.
|
||||
Reference in New Issue
Block a user