Skip to content

Architecture Overview

Doable is a monorepo of four runnable units (web, api, ws, postgres) plus four shared packages (db, shared, docore, dovault). It's small enough to read end-to-end in a day, but designed so each piece can be replaced independently.

High-level diagram

Doable architecture — Browser connects to Web, API, and WebSocket services, backed by shared packages and PostgreSQL

Text-based diagram (for accessibility)
                      ┌────────────────────────────┐
                      │        End user / browser  │
                      └────────────┬───────────────┘
                                   │  HTTPS / WSS
                          ┌──────────────────┐
                          │ nginx / Caddy    │   TLS termination
                          └─────┬─────┬──────┘
                                │     │ /ws
                /api,/auth      │     ▼
                                │  ┌───────────┐
                                │  │   ws      │  WebSocket service (Yjs CRDT)
                                │  └─────┬─────┘
                                ▼        │
                       ┌─────────────┐   │   internal HTTP
                       │     api     │◀──┘
                       │  (Hono)     │
                       └─────┬───────┘
       ┌─────────────────────┼─────────────────────────────┐
       ▼                     ▼                             ▼
 ┌───────────┐       ┌──────────────┐             ┌────────────────┐
 │ Postgres  │       │  docore      │             │   web (Next)   │
 │ (pgvector)│       │  AI engine   │             │  Server pages  │
 └───────────┘       └──────┬───────┘             └────────────────┘
                            │ spawns
                   ┌──────────────────┐
                   │ Copilot CLI / …  │  AI provider subprocess
                   └──────────────────┘
                            │ tool calls
                   ┌──────────────────┐
                   │  dovault (jail)  │  cwd + permission sandbox
                   └────────┬─────────┘
                   ┌──────────────────┐
                   │ User project FS  │  PROJECTS_ROOT/<projectId>/
                   │  + Vite dev srv  │
                   └──────────────────┘

The four runtime units

1. apps/web — Next.js 15 frontend

  • React 19, Tailwind 4, Monaco editor, Yjs.
  • Server Components for the dashboard shell.
  • Client Components for the editor, chat, and preview iframe.
  • Uses NEXT_PUBLIC_API_URL and NEXT_PUBLIC_WS_URL to talk to the backend.
  • Authenticates via JWTs stored in HttpOnly cookies (set by the API).

2. services/api — Hono REST API (Node.js)

  • Single Node process, ~80 route files mounted in src/routes.ts.
  • Owns: auth, projects, files, AI chat, billing, integrations, GitHub sync, deployments, admin, marketplace, analytics.
  • Runs migrations, exposes /health.
  • Spawns one Vite dev server per active user project (in a sandboxed child process via dovault).
  • Acts as a reverse proxy for the live preview at /preview/:projectId/*.

3. services/ws — WebSocket server

  • Hono + ws with Yjs for CRDT documents.
  • One Y.Doc per project; all collaborators connect to it.
  • Awareness protocol carries cursors, selections, and presence.
  • Talks back to the API via INTERNAL_SECRET-signed HTTP for auth and persistence.

4. postgres — PostgreSQL 16

  • Extensions: pgcrypto (UUID generation), pgvector (embeddings), pg_trgm (fuzzy search).
  • Schema lives in packages/db/migrations/*.sql, applied lexicographically by pnpm db:migrate.
  • Doable does not use an ORM — queries live in packages/db/src/queries/*.ts using the postgres driver.

Shared packages

Package Purpose Used by
@doable/db Connection + typed query helpers api, ws
@doable/shared Constants, types, KV store abstraction (memory/Redis) All
@doable/docore The AI agent engine — wraps Copilot SDK, normalizes events, manages tool policies api
@doable/dovault Runtime jail — Node permissions + OS resource limits api (when spawning child processes)

See Monorepo Layout for the complete file tree.

Request lifecycle examples

A user message in the chat

  1. Browser → POST /chat/:projectId/messages (API) with the new prompt.
  2. API authenticates the JWT, loads the project, and starts a streaming response (SSE).
  3. API delegates to docore.engine. Docore opens / resumes a session against the configured provider (Copilot CLI, Anthropic, OpenAI).
  4. The provider streams deltas, tool calls, and final messages back through docore's EventBus.
  5. event-mapper normalizes them; the API forwards normalized events as SSE chunks to the browser.
  6. Tool calls (file edits, shell, fetch) are evaluated against the workspace's policy. Anything filesystem-bound runs inside a dovault jail rooted at PROJECTS_ROOT/<projectId>/.
  7. After each file write, the WS service is notified so other collaborators see the new content.

A live preview load

  1. Browser navigates the preview iframe to /preview/:projectId/index.html.
  2. API checks if a Vite dev server is already running for that project (projects/dev-server.ts). If not, it spawns one, sandboxed by dovault, on a free localhost port.
  3. API proxies the request to that internal port.
  4. The API injects the visual edit bridge script into HTML responses so the parent (Doable editor) can pick clicks and update text in place.

Key design decisions

  • Single API process, no microservices. Easier to deploy, debug, and back up. The WS service exists only because long-lived sockets are awkward to share.
  • Filesystem as source of truth for project files. AI agents work best on real files; the DB only stores metadata. The project_files table is a cache for fast reads from the dashboard.
  • tsx watch in production for API/WS. Acceptable trade-off: instant updates on git pull, no separate build step. The web app is built normally for SSR perf.
  • Bind to 127.0.0.1 everywhere; one TLS proxy in front. See Network Binding.
  • No Redis required. The kv-store abstraction defaults to in-memory; flip on Redis only when you scale beyond one API replica.