2026-06-10 07:53:42 +00:00
2026-06-10 07:49:40 +00:00

ClaudeDo Online Inbox

Optional online mirror of the ClaudeDo desktop app's Idle task backlog. Lets the single owner view their lists and jot new tasks from a phone or browser. The desktop app (local, .NET/SQLite) remains the source of truth; this service only stores/serves data — it never executes tasks.

Live: https://claudedo.kuns.dev · API base: https://claudedo.kuns.dev/api

Governing rule

The online store mirrors exactly the desktop's Idle tasks. A task exists online only while it is Idle; queuing it on the desktop deletes it here. Running/Done/Failed/review tasks never appear online.

  • Lists — desktop → online only (full-replace catalog).
  • Idle tasks — two-way creation: the desktop pushes its Idle tasks (PUT) and pulls web-created ones (consumed=false → import → consume).

Stack

Nuxt 3 (Vue 3, TypeScript, Bun) running as an SPA (ssr: false) with Nitro server routes as the API, on shared PostgreSQL, behind Zitadel auth, deployed via Coolify.

  • Web client login: @kuns/zitadel-auth (vendored under vendor/).
  • API token validation: jose against Zitadel JWKS.
  • DB access: postgres (postgres.js), parameterized queries only.

API

Every /api/** route requires a valid Zitadel access token (Authorization: Bearer …) belonging to the owner. Missing/invalid/expired → 401. No anonymous access.

Method & path Caller Behaviour
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/{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
PUT /api/tasks/{id} desktop Body {listId, title, description?}. Idempotent upsert (source=desktop on insert). → 201 (new) / 200 (existing)
DELETE /api/tasks/{id} desktop Idempotent. → 204 (even if absent)
GET /api/tasks?consumed=false desktop → 200 [{id, listId, title, description, createdAt}] web tasks not yet imported
POST /api/tasks/{id}/consume desktop Sets consumed=true. Idempotent. → 200; 404 if unknown

Task ids are a shared GUID space: web-created ids are reused verbatim by the desktop on import; all task writes are idempotent upserts keyed on id.

Zitadel configuration (for the desktop client)

Both the desktop and the web client authenticate as the same Zitadel user (the owner). Project ClaudeDo has two PKCE apps (both issue JWT access tokens):

Web Desktop
Client id 376787352019861775 376787352137302287
App type User-Agent (SPA) Native
Auth method PKCE (none) PKCE (none)
Grants authorization_code authorization_code, refresh_token
Redirect https://claudedo.kuns.dev/auth/callback http://localhost:8765/callback, http://127.0.0.1:8765/callback

Desktop OnlineInbox settings:

  • Issuer: https://auth.kuns.dev
  • Client id: 376787352137302287
  • Scopes: openid profile email offline_access urn:zitadel:iam:org:project:id:376787351902355727:aud
    • offline_access → refresh token for headless re-auth.
    • the …:aud scope puts the project id into the token's aud so the API validates it.
  • Flow: Authorization Code + PKCE on a loopback redirect for the initial interactive login, then refresh-token grant headlessly. Store the refresh token securely.
  • API base URL: https://claudedo.kuns.dev/api

Project id: 376787351902355727. Owner user id (sub): 365090688972947729 (mika@kuns.dev).

Environment variables

Server-only values are read from process.env at runtime (set them in Coolify):

Var Purpose
DATABASE_URL postgres://mika:…@l8kogcggsc80sgcgk8kswww4:5432/claudedo (shared PG, internal host)
ZITADEL_ISSUER https://auth.kuns.dev
ZITADEL_AUDIENCE accepted audiences (CSV): web id, desktop id, project id
ALLOWED_USER_IDS owner sub allowlist (CSV)
WEB_ORIGIN CORS allowed origin (https://claudedo.kuns.dev)

Public web-client config is baked at build time (non-secret) via Dockerfile build args (NUXT_PUBLIC_ZITADEL_ISSUER, NUXT_PUBLIC_ZITADEL_CLIENT_ID, NUXT_PUBLIC_ZITADEL_PROJECT_ID); override with --build-arg if the ids change.

See .env.example for local development.

Development

bun install
# Point at a local/tunnelled Postgres and apply the schema:
DATABASE_URL=postgres://… bun run migrate
DATABASE_URL=postgres://… bun run dev      # http://localhost:3000

# Tests (need a Postgres test DB):
DATABASE_URL=postgres://…/claudedo_test bun run test

Local API smoke without a Zitadel token: set AUTH_DEV_BYPASS=1 (dev mode only — it is dead-code-eliminated from production builds).

Database

DB claudedo on the shared PostgreSQL instance. Schema (server/utils/schema.ts) is applied idempotently on server startup by server/plugins/migrate.ts (advisory-locked) and via bun run migrate.

Deployment

Coolify builds the Dockerfile (Bun build → Node runtime) on push to Gitea; Traefik routes claudedo.kuns.dev with automatic SSL.

Description
No description provided
Readme 277 KiB
Languages
TypeScript 69.7%
Vue 27.9%
Dockerfile 1.8%
JavaScript 0.6%