Restructures the project into a pnpm workspace mirroring multica-ai/multica on a unified TypeScript stack so the product can run as a hosted cloud app that pairs with a local `od` daemon CLI for executing code-agents on the user's machine. Adds: - apps/web — Next.js 16 App Router (auth, projects, daemon endpoints, SSE) - apps/cli — `od` daemon CLI (login via local-listener OAuth, register, claim, spawn agents, stream task messages) - packages/db — Drizzle schema with Postgres + SQLite dialects, runtime- selected by DATABASE_URL prefix - packages/shared — zod request schemas, JWT helpers, token mint/hash Sharing primitives: multica-style workspace_invitation (email → membership) plus a per-project share link (`/share/<token>` with view/comment/edit role) so a single project can be sent as a public URL without granting workspace access. Legacy Vite + Express stack (src/, daemon/) preserved alongside under `legacy:*` npm scripts. Components, S3 file storage, and skill route handlers are out of scope for this scaffold and tracked in HANDOFF.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.9 KiB
Architecture: cloud UI + local daemon (Next.js 16 refactor)
Supersedes
architecture.mdfor the new stack. Legacy Vite + Express docs remain valid for thesrc/anddaemon/directories preserved on this branch.
This branch refactors Open Design from a Vite SPA + Express daemon (localhost-only) into a cloud-deployable Next.js 16 App Router product paired with a local od daemon CLI. Modeled on multica-ai/multica, unified onto a TypeScript stack.
┌──────────────────────────┐
┌──────────┐ │ │
│ Browser │ ─── HTTP/SSE ─────► │ apps/web (Next.js 16) │
└──────────┘ │ • Login / share / UI │
│ • Drizzle → PG/SQLite │
│ • SSE fan-out hub │
└─────────┬─▲──────────────┘
│ │
POST /tasks claim │ │ task_messages
GET /wakeups (SSE) │ │ (POST per chunk)
▼ │
┌──────────────────────────┐
│ apps/cli (`od daemon`) │
│ • Profile @ ~/.od │
│ • Spawns claude/codex/… │
└──────────────────────────┘
Auth flow (browser-callback OAuth, no device codes)
od loginbinds127.0.0.1:<rand>, opens browser to${WEB}/login?cli_callback=…&cli_state=….- User signs in (email 6-digit code).
- After verify, the web app redirects to the local listener with a freshly minted PAT (
od_pat_…). - PAT is stored at
~/.od/profiles/<name>/config.json(mode 0600). State string is verified to prevent CSRF.
Daemon registration
od daemonboot:- PAT-authenticates
POST /api/daemon/registerwith hostname, OS, runtimes detected on PATH. - Server returns a workspace-scoped daemon token (
od_dt_…). Token is hashed at rest.
- PAT-authenticates
- Daemon then uses the daemon token for everything else; PAT is no longer touched until next
od login. - Heartbeats every 15s (
POST /api/daemon/heartbeat).
Task flow
- Browser calls
POST /api/projects/<id>/chat→ server inserts anagent_taskrow (status =queued) and pings the in-process pub/sub. - Daemon's open SSE connection on
GET /api/daemon/wakeupsreceives atask_availablehint or the daemon notices on the next 3s polling cycle. - Daemon
POST /api/daemon/runtimes/<runtimeId>/tasks/claimperforms an atomicUPDATE … WHERE status='queued'lease. HTTP claim is the source of truth; SSE wakeup is a latency optimization. - Daemon spawns the matching local agent (claude/codex/…), tees stdout/stderr to
POST /api/daemon/tasks/<taskId>/messageschunked byseq. - Browser opens
GET /api/tasks/<taskId>/stream(SSE). Server replays persistedtask_messagerows, then forwards new chunks live via the pub/sub. - On
kind: end, the task row flips tosucceeded/failed.
This mirrors multica's "HTTP-claim + WS-wakeup" hybrid, ported to TS + SSE (which Next.js route handlers support without a custom server).
Data model (Drizzle)
Single Drizzle schema with both Postgres and SQLite mirrors. Runtime selects by DATABASE_URL prefix:
postgres://…→ cloud (Vercel/Neon/Supabase)file:./.od/app.sqlite→ self-hosted single node
Tables:
| table | purpose |
|---|---|
user |
one row per signed-in human |
email_verification_code |
6-digit codes for email login |
workspace + member |
tenancy + roles (owner/admin/member) |
workspace_invitation |
email-based invites, 7-day TTL |
personal_access_token |
issued during CLI login, used to bootstrap daemon |
daemon_registration |
one per paired device (workspace-scoped daemon token, hashed) |
project |
belongs to workspace |
project_share_link |
per-project token + role (view/comment/edit) + optional expiry |
conversation / message |
chat history per project |
project_file |
metadata; bytes go to S3/GCS or the daemon's local disk |
agent_task |
queued/claimed/running/succeeded/failed/cancelled |
task_message |
streamed chunks per task (replayable) |
Sharing & collaboration
Two complementary primitives:
-
Workspace invitations (multica-style):
POST /api/workspaces/<wsId>/invitations { email, role }mints aod_inv_<token>link. Accepting it via/invitations/<token>adds the user to themembertable. Recommended for ongoing collaborators who should see all workspace projects. -
Project share links (NEW vs multica):
POST /api/projects/<id>/share { role: view|comment|edit, expiresInDays? }mints${PUBLIC_APP_URL}/share/<token>. Anyone with the link can open the project at the issued role. Cookie binds the share scope to the specific project path. Revoke from the project's settings.
The ACL helper lib/access.ts resolves either path uniformly: member first, then share cookie (or ?share= query). The page returns a single ProjectAccess record so server components and route handlers gate identically.
Deploy targets
Cloud (Vercel + Postgres)
DATABASE_URL=postgres://…
AUTH_JWT_SECRET=… # 32+ random bytes
PUBLIC_APP_URL=https://your-app.vercel.app
EMAIL_PROVIDER=resend
RESEND_API_KEY=…
EMAIL_FROM=Open Design <noreply@your-domain>
The web app is fully serverless. SSE works on Vercel (route handlers stream text/event-stream) within the platform timeout — for very long agent runs, host on Fly/Railway/your own infra instead.
Self-host single node
DATABASE_URL=file:./.od/app.sqlite
AUTH_JWT_SECRET=…
EMAIL_PROVIDER=console
Run pnpm --filter @open-design/web build && pnpm --filter @open-design/web start. SQLite is fine up to a few hundred users.
Multi-instance
The in-process pub/sub at apps/web/lib/realtime.ts is the only piece that doesn't horizontally scale. Swap for Redis Pub/Sub or Postgres LISTEN/NOTIFY keeping the same call signature; nothing else needs to change.
Migration from the legacy stack
The legacy Vite + Express daemon (src/, daemon/) is preserved in this branch under the legacy:* npm scripts so we can run both stacks side-by-side during the transition. Skills and design-systems directories are unchanged and consumed at request time by the new Next.js app via SKILLS_ROOT / DESIGN_SYSTEMS_ROOT.
What's NOT yet ported
These belong in follow-up PRs and are deliberately out of scope for the scaffold:
- File storage adapter (S3/GCS) for the cloud —
project_file.storageKeyis shaped for it but no upload route exists yet. - Skill / design-system route handlers — the legacy Express endpoints aren't mirrored on Next.js yet.
- The 28 React components from
src/components/— only the project shell page exists; chat composer / file viewer / sketch editor still need porting. - Anti-slop linter (
daemon/lint-artifact.js). - Anthropic SDK browser-mode fallback.