eli workspace
eli workspace — overview & capability map
eli is a files-first, agent-native, async-collaborative “second brain”: a single Next.js 16 codebase that runs in two modes behind one storage abstraction — plain markdown on your disk, or a multi-tenant SaaS on Supabase.
About this section
eli-secondbrain codebase) — a sibling of the eli.ai platform documented in the other tabs. Every diagram here follows one convention: a simplified and a detailed view, at both the system (component) level and the data-flow (runtime) level. Download the full 25-page draw.io bundle for the complete per-cluster diagram set.One codebase, two modes
- Local mode (
ELI_MODE=local, the default —env.ts:15): a guest, no-account app onhttp://localhost:3737(package.json→next dev --turbopack -p 3737). Your notes are plain markdown files in a vault directory you pick; everything else — FTS5 full-text index, wikilink graph, embeddings, versions — lives in a rebuildable per-vault SQLite database at~/.eli/index/<hash>.db. Delete the index and eli rebuilds it from the files. - SaaS mode (
ELI_MODE=saas): a multi-tenant hosted product on Supabase — Postgres (+tsvector,pgvector) with Row-Level Security as the security boundary (33 policies over 9 tables, pgTAP-verified in CI), magic-link/OAuth auth, workspaces with role-based memberships, comments, share links, and Realtime change notifications.
Both modes render the same RSC/component tree: route groups app/(local)/ and app/(saas)/ share components/* and app/_actions/*, and every surface talks to storage through the StorageAdapter interface (lib/storage/adapter.ts), resolved by getStorage() (lib/storage/index.ts:20). “Agent-native” is not a bolt-on: a 16-tool MCP server (lib/mcp/server.ts) is served over stdio (bin/eli-mcp.mjs) and streamable HTTP (app/api/mcp/[transport]/route.ts), agents that bypass MCP and edit .md files directly are picked up by a chokidar watcher and versioned with authorKind: 'agent', and an OpenAI-compatible /v1 facade (app/v1/) makes the vault a drop-in RAG backend for any OpenAI-SDK client.
The four invariants
Everything in this doc set hangs off four load-bearing invariants (docs/ARCHITECTURE.md §1):
- The markdown file on disk (or
body_mdin Postgres) is the source of truth. FTS fragments,note_links, embeddings, the folder tree — all rebuildable derived index. - Every UI surface goes through
getStorage(). No component, server action, API route, or MCP tool importsLocalAdapterorSupabaseAdapterdirectly (ADR-0004). Swapping modes is a config switch, not a refactor. - No raw HTML controls outside
components/ui/*— all controls are shadcn/ui primitives, so theming, a11y, and keyboard handling stay uniform. ~/.eli/config.jsonis the single user-scope state file (active vault, tokens, favourites, pairings). Vault contents never leak into it.
Phase status
Per the phase-file headers, docs/IMPLEMENTATION-LOG.md, and the git log (134 commits) — not the status table in docs/phases/README.md, which still claims Phases 2–7 are “not started” and is stale, as are parts of docs/ARCHITECTURE.md §11’s “not yet built” inventory. This doc set reflects code reality, cross-checked against the repo source at the current head (a8c4269).
| Phase | Scope | Status |
|---|---|---|
| P1 | Local MVP (vault, editor, FTS, palette, versions, SSE live refresh) | DONE (8c9b649), carry-overs closed in later sweeps |
| P2 | Git versioning + SaaS auth + workspaces | DONE — all 22 tasks / 9 tracks (1708adc) |
| P3 | MCP server + agent integrations | DONE — all 13 tracks (0287cbb) |
| P4 | Async collaboration (anchors, diffs, comments, shares, conflicts) | DONE — data path closed, all UI carry-overs landed |
| P5 | Clipper + AI (gateway, embeddings, hybrid search, chat, /v1) | DONE in scope — only the Chrome MV3 extension declared out of scope |
| P6 | Templates + mobile + PWA | DONE in scope — Tauri shell and git-as-bridge sync declared out of scope |
| P7 | Stretch spikes (graph view, CRDT, voice, …) | NOT STARTED, except P7-AGENT-01 usage dashboard (4db76b0) |
Build health at the last log entry: 712 unit tests (+1 intentional skip), 25/25 Playwright e2e, 11 MCP harness tests, typecheck/lint/build green.
How to read this doc set
The Workspace tab is nine cluster pages plus one end-to-end journeys page, backed by the downloadable multi-page draw.io bundle. The central rule, applied uniformly:
Every subject gets BOTH a simplified view AND a detailed view, at BOTH the system (component/architecture) level AND the data-flow (runtime/sequence) level — and every cluster page explains how the subject works SEPARATELY (standalone) and TOGETHER (composed with the other clusters).
Concretely, each cluster page contains: System view — simplified (≤10-node component sketch), System view — detailed (full component map with file paths), Data-flow — simplified (the happy-path pipeline), Data-flow — detailed (sequence diagram with local-vs-SaaS branches and file:line anchors), plus a Standalone vs composed section. The draw.io bundle mirrors this: one page per cluster with labeled SIMPLIFIED and DETAILED bands, plus the four platform-level pages that correspond to the diagrams on this page. The journeys page replays three complete journeys (local-first solo loop, SaaS team loop, external-agent loop) across all clusters.
Cross-cutting surfaces that the multi-agent analysis treated separately — trash/bulk actions and the daily digest — are folded in where they live: storage modeling in core data & storage, the trash/bulk lifecycle in versioning & history, the digest builder in the AI platform, and the cron/prune machinery in platform ops.
| # | Cluster | One-line purpose | Key dirs |
|---|---|---|---|
| C1 | Core data & storage | StorageAdapter contract + 4 implementations (Local, Supabase, Memory, JsonFile), both DB schemas, vault scan/index pipeline, chokidar watcher, config store, mode bootstrap | lib/storage/, lib/db/, lib/domain/, lib/watch/, lib/config/, env.ts, instrumentation.ts |
| C2 | Authoring & navigation UX | BlockNote/raw editor with debounced autosave + conflict banner, sidebar/palette/inspector shell, keybindings, theming, mobile & a11y | components/, app/(local)/, app/_actions/, lib/keybindings/ |
| C3 | Search & embeddings | Auto-embed queue (30s debounce) → 1536-dim vectors (SQLite BLOB / pgvector); FTS + vector KNN fused with weighted RRF (k=60, fts 0.7 / vector 0.3) | lib/search/, lib/ai/embeddings.ts, lib/ai/auto-embed-queue.ts, app/api/search/hybrid/, scripts/embed-backfill.mjs |
| C4 | AI platform | Gateway-routed model tiers, three-tier BYOK key resolver, seven /api/ai/* features, persisted RAG chat with function tools, daily digest, usage metering, /v1 OpenAI facade | lib/ai/, app/api/ai/, app/v1/, components/ai/, lib/chat/ |
| C5 | Agent-native surface (MCP) | 16 MCP tools over stdio + streamable HTTP, PAT/local-token auth, rate limits, five-agent setup wizard, AGENTS.md generator, OpenClaw memory compat | lib/mcp/, bin/, app/api/mcp/, lib/setup/, lib/agents-md/, lib/openclaw/ |
| C6 | Versioning, history & recovery | Debounced git auto-commit (local bookkeeping layer) + git-free section-level diff/restore UI reading the adapters’ versions table; trash/bulk lifecycle | lib/git/, components/diff/, app/_actions/diff.ts, app/_actions/bulk.ts |
| C7 | SaaS collaboration | Supabase auth, workspaces/memberships/invites under RLS, section-anchored comments, share links /s/[token], Realtime toasts, audit log | app/(saas)/, lib/auth/, lib/share/, components/comments/, components/realtime/, lib/email/ |
| C8 | Capture & templating | Bookmarklet + /api/clipingest, PWA share-target, zip/JSON import & export, template language + editor + AI generation | lib/clipper/, lib/templates/, app/api/clip/, app/api/import/, app/api/export/, app/share/, app/manifest.ts |
| C9 | Platform ops | Cron (digest 13:00, trash-prune 04:00 UTC), events bus + SSE live refresh, dev/test endpoints, seeding & bench CLIs, native-ABI self-heal, MCP bin packaging, full test pyramid + CI gates | app/api/cron/, app/api/events/, app/api/dev/, scripts/, tests/, .github/workflows/ |
| 10 | End-to-end journeys | Three composed walkthroughs: local-first solo loop, SaaS team collaboration loop, external-agent (MCP) loop | spans all of the above |
Platform system view — simplified
The 10-second mental model: two kinds of principals (humans and agents), three kinds of surfaces (UI, MCP, HTTP APIs), one storage seam, two backends, one AI gateway.
Everything below elaborates this picture; nothing contradicts it. The one asymmetry worth memorising now: in local mode the .md file is written first (atomic tmp+rename) and the index catches up; in SaaS mode the write is a single transactional notes_save() RPC with optimistic concurrency on content_hash.
Platform system view — detailed
Each cluster’s 2–4 core components and every material cross-cluster edge. Colors encode cluster identity and are reused in the draw.io pages.
Notable structural facts this diagram encodes:
- C1 is the only cluster everyone depends on. ADR-0004 makes
StorageAdapterthe binding contract; a shared conformance suite (tests/adapter/contract.ts) must pass for every adapter. - C5 and C4 never talk to each other’s tool registries. MCP tools (
*_artifactnaming) and AI-SDK chat tools (*_notenaming,lib/ai/tools.ts) are deliberately parallel surfaces over the same storage (docs/tools.md:11-14). - The save fan-out is where clusters compose: one
updateNote()in C1 triggers C3 (embed queue), C6 (git queue), C9 (events bus → SSE), and — in SaaS — C7 (Realtime notification). - C9’s SSE channel and C7’s Realtime toast are mode-twin mechanisms for the same UX outcome (other tabs/users see the change): in-process events bus locally, Supabase Realtime in SaaS.
Integration map — how the clusters compose
The clusters as nodes; every material runtime dependency as a labeled edge.
| Producer | Consumer | Mechanism | Code anchor |
|---|---|---|---|
| C2 | C1 | Server actions call getStorage() for every mutation/read | app/_actions/notes.ts → lib/storage/index.ts:20 |
| C5 | C1 | Every MCP tool resolves McpContext → StorageAdapter; no mode branching in tool code | lib/mcp/context.ts, lib/mcp/server.ts |
| C5 | C3 | search tool routes kind: semantic|hybrid through the hybrid orchestrator | lib/mcp/tools/search.ts → lib/search/hybrid.ts |
| C5 | C8 | apply_template delegates to the adapter-level template engine | lib/mcp/tools/template.ts → lib/templates/ |
| C8 | C1 | handleClip / import / export write and read notes via storage | lib/clipper/, app/api/clip/route.ts, app/api/import/, app/api/export/ |
| C7 | C1 | Session cookie + eli_ws workspace cookie select the RLS-scoped SupabaseAdapter | middleware.ts, lib/storage/supabase* |
| C1 | C3 | The note-save path arms a per-note 30s embed debounce (DEBOUNCE_MS = 30_000); trigger sits in the save action for UI edits — MCP/watcher edits rely on the batch embed route or backfill CLI | app/_actions/notes.ts:97-98 → lib/ai/auto-embed-queue.ts |
| C1 | C6 | Local saves enqueue relPath into the per-vault commit queue (default debounceSec: 30) | lib/storage/local/writes.ts:331 (enqueueCommit) → lib/git/commit-loop.ts:108 |
| C1 | C9 | LocalAdapter writes and the chokidar watcher publish to the in-process events bus | lib/watch/events-bus.ts, lib/watch/chokidar.ts |
| C9 | C2 | SSE stream → <VaultEventsBridge> debounced router.refresh() | app/api/events/route.ts, components/vault-events-bridge.tsx |
| C4 | C3 | Chat injects hybrid-search hits as RAG system context | app/api/ai/chat/route.ts:178 (hybridSearch) |
| C3 | C4 | Embedding generation goes through the gateway embed tier + key resolver | lib/ai/embeddings.ts, lib/ai/resolver.ts |
| C4 | C1 | Chat function tools (read_note, update_note, …) hit storage; writes audited, capped at 6 steps | lib/ai/tools.ts |
| C2 | C4 | Inline AI actions and the “Ask your vault” chat UI call /api/ai/* | components/editor/, components/ai/vault-chat.tsx |
| C9 | C4 | Vercel Cron (13:00 UTC) triggers the digest runner | vercel.json, app/api/cron/digest/route.ts → lib/ai/digest-runner.ts |
| C9 | C1 | Vercel Cron (04:00 UTC) purges trash older than 30 days | app/api/cron/trash-prune/route.ts |
| C6 | C1 | Diff UI reads the adapters’ append-only versions table (never git) | app/_actions/diff.ts, lib/storage/local/versions.ts |
| C7 | C2 | Supabase Realtime note:updated → refresh toast in other members’ tabs | components/realtime/note-watcher.tsx |
Running a cluster in isolation
The seams are real — meaningful sub-products fall out of subsets of clusters:
- C1 + C2 = a complete offline notes app. Vault picker, BlockNote editing, buckets/tags/folders, FTS search, version history — no network, no API keys, no git. This was literally Phase 1 as shipped.
- C1 + C5 = a headless agent vault.
node bin/eli-mcp.mjswithVAULT_PATHboots aLocalAdapterdirectly — no Next.js server, no UI. An agent gets full CRUD, FTS search, versions, comments, and templates against plain files. - C1 + C3 + C4 = a RAG service over a vault. Point any OpenAI-SDK client at
/v1/chat/completionswith a PAT and get hybrid-search-grounded answers; no eli UI involved. - C1 + C7 = collaboration without AI. A team on Supabase gets workspaces, RLS-scoped notes, comments, invites, shares — no gateway key configured, AI routes simply unavailable.
- C6’s git layer and C9 are shed-able.
ELI_DISABLE_GIT=1turns the commit loop into a no-op (versions still recorded in C1’sversionstable);ELI_DISABLE_LIVE_EVENTS=1silences SSE (the e2e suite runs this way). The product degrades gracefully, never breaks. - C8 standalone is thin by design — the clip handler, importers, and template engine are libraries over C1; the bookmarklet and PWA share-target are just alternative front doors.
The full loop
Composed, the clusters form one closed loop: content enters via C2 (typing), C8 (clip/share/import), or C5 (agent tools); C1 persists it file-first with a version row and fans out to C3 (embedding), C6 (git commit), and C9 (SSE/Realtime refresh); C3 makes it findable by keyword + meaning; C4 reads it back as RAG context for chat and digests, and can write back through the same storage seam (audited, attributed); C7 layers humans-in-the-loop on top (comments, shares, conflict snapshots); C6 guarantees any state is recoverable; and C9 keeps the whole thing observed, tested, and pruned. Every write — human, clipper, agent, or cron — is the same write: one StorageAdapter call, one authorKind-attributed version row, one fan-out.
Data-flow spine — simplified
The life of a note, compressed to one pipeline:
Data-flow spine — detailed
The full life of one note across all nine clusters, with local-vs-SaaS branches where the paths diverge.
Divergence summary — the spine is identical in shape in both modes; only four hops differ:
| Hop | Local | SaaS |
|---|---|---|
| Persist | atomic tmp+rename .md write, then index (lib/storage/local/writes.ts) | transactional notes_save() RPC, OCC on content_hash |
| Keyword index | SQLite FTS5, bm25 (lib/db/) | Postgres tsvector, ts_rank_cd |
| Change propagation | chokidar + events bus → SSE (lib/watch/, app/api/events/) | Supabase Realtime (components/realtime/note-watcher.tsx) |
| Cold-storage history | debounced git commit (lib/git/commit-loop.ts) | none — versions table only |
The diagrams on this page correspond to the four platform pages of the draw.io bundle — “Platform — System (Simplified)”, “Platform — System (Detailed)”, “Platform — Integration Map”, and “Platform — Data-Flow Spine”; per-cluster pages follow the same color coding. Download eli-workspace-architecture.drawio.