Decant Runtime Architecture#
Decant runs as a single Bun + TypeScript process that owns the SQLite archive, CLI, local React UI, and API/watch loop simultaneously . There is no background daemon, bearer-token boundary, separate API process, or hosted service — everything executes in the one decant process .
~/.claude + ~/.codex
|
v
Bun + TypeScript Decant process
parse -> enrich -> ingest + economics vectors -> SQLite WAL + FTS5
|
+--> CLI reads / JSON
+--> local React UI + JSON routes + SSE
This design replaced a three-process Rust/Phoenix/Swift stack in July 2026 . The pre-cutover tree is preserved in the signed pre-typescript tag.
The serve Command#
decant serve is the primary runtime entry point. It binds http://127.0.0.1:3000 by default and runs five subsystems in one process :
- Archive owner — the single process that holds the SQLite WAL connection
- Source watcher — filesystem + sweep watcher over
~/.claude/projects/and~/.codex/sessions/ - HTTP API — all
/api/*routes per the OpenAPI 3.1 contract - SSE stream — real-time event delivery at
GET /api/events - React UI — served as Bun fullstack HTML imports (no separate Vite dev server)
Startup Sequence#
The CLI entry (src/cli.ts serve handler) calls serveApp(), which delegates to serve() in src/server.ts. Startup proceeds in strict order:
- Bind the HTTP port first — before touching the archive. A port conflict causes a fast failure rather than leaving an orphaned DB-owning watcher.
- Open DB and hydrate economics — archive is opened, derived metadata backfilled, economics cache pre-warmed.
- Start source watcher — only if syncing is enabled (skipped under
DECANT_NO_SYNC/--no-sync).
Incoming requests while the DB is still initializing return a 503 serviceStartingResponse .
Source Watching and Sync Coordination#
Watch-triggered ingests run in a worker thread via workerSyncRunner(), keeping database writes off the HTTP event loop.
Overlapping sync requests — from the watcher, a periodic sweep, or a manual POST /api/sync — are coalesced by createSyncCoordinator(): only one physical sync worker runs at a time; concurrent callers share the same Promise and receive fanned-out progress updates.
SSE Events#
GET /api/events returns text/event-stream. Events are broadcast to all connected clients via publishServerEvent(). Watch results flow through applyWatchEvent(), which updates shared syncStatus and republishes to SSE exactly once .
| Event name | Meaning |
|---|---|
hello | Connection acknowledgement |
ping | Heartbeat (~5 s) |
ready | Source watcher initialized |
sync_progress | Bounded in-flight sync snapshot |
sync | Terminal successful sync report |
archive_updated | Archive-derived UI data changed |
error | Watcher or sync failure |
stopped | Source watcher stopped |
Graceful Shutdown#
Shutdown coordinates five subsystems in parallel: economics cache disposal, watcher stop, sync coordinator close, HTTP server stop, and DB close.
Key Invariants#
| # | Invariant | Implication |
|---|---|---|
| 1 | One process owns SQLite | No daemon, token file, or second app may open the DB |
| 2 | Core modules stay print-free | CLI output/exit-code policy lives only in src/cli.ts |
| 3 | Local-first only | No outbound network calls; loopback bind by default |
| 7 | Routes are a documented local API | docs/api/openapi.yaml is the contract; /api/openapi.json is its runtime form |
Access Control#
The API has no authentication . The loopback bind (127.0.0.1) is the sole boundary for local-only deployments. For non-loopback binds (e.g., Docker), trusted peers are resolved by replacement precedence: --trusted-peer → DECANT_TRUSTED_PEERS → DECANT_TRUST_DEFAULT_GATEWAY=1 . Any untrusted remote source receives 403 forbidden_remote.
Primary Source Files#
| File | Purpose |
|---|---|
src/cli.ts | serve command handler; passes watch config to serveApp() |
src/server.ts | HTTP server, watcher, SSE, sync coordinator, graceful shutdown |
docs/api/openapi.yaml | OpenAPI 3.1 contract for all /api/* routes |
docs/api/routes.md | Operational semantics: access control, SSE events, pagination |
AGENTS.md | Project invariants and contributor guidance |