Skip to documentation

Capability clusters

C1 · Core data & storage

The substrate everything else in eli stands on: a single storage contract with two production backends — markdown files on disk with a rebuildable SQLite index, and multi-tenant Supabase Postgres where RLS is the security boundary — plus the helpers, schemas, watcher, and bootstrap that keep it fresh.

This cluster is a single StorageAdapter interface (lib/storage/adapter.ts) implemented four times — LocalAdapter (markdown files on disk as source of truth plus a rebuildable per-vault SQLite index), SupabaseAdapter (multi-tenant Postgres where RLS is the security boundary), MemoryAdapter (test double), and JsonFileAdapter (single-file portability reference). It also owns mode selection (ELI_MODE), the vault config store (~/.eli/config.json), the markdown/frontmatter/wikilink helper library, both database schemas and migration chains (SQLite FTS5; Postgres tsvector + pgvector + RLS), the walker → scanner → incremental-index pipeline, the chokidar watcher + in-process event bus that keep the local index fresh, and process bootstrap (env.ts, instrumentation.ts, middleware.ts). Every Server Action, RSC page, API route, and MCP tool reaches notes exclusively through getStorage() (lib/storage/index.ts) — components never import an adapter directly.

  • Modes: local (files + SQLite) and saas (Supabase Postgres + RLS), selected by ELI_MODE.
  • Key dirs: lib/storage/, lib/db/, lib/domain/, lib/index/, lib/watch/, lib/config/, root bootstrap (env.ts, instrumentation.ts, middleware.ts).
  • Depends on: better-sqlite3, chokidar, supabase-js, gray-matter, diff-match-patch.
  • Feeds: every other cluster — authoring UX (02), search (03), AI platform (04), agent surface (05), versioning (06), SaaS collaboration (07), capture/templates (08), platform ops (09).

Capabilities

CapabilityWhat it doesEntry pointsModes
Mode selectiongetStorage() returns LocalAdapter pinned to the active vault (ELI_VAULT env overrides config.jsonactiveVaultId) or SupabaseAdapter pinned to the request's workspace (eli_ws cookie + session). Env schema hard-fails on missing mode requirements (env.ts:103-133); route groups app/(local) / app/(saas) call notFound() for the wrong mode.lib/storage/index.ts getStorage(), env.tsboth
Note CRUD, file-first (local)createNote/updateNote write frontmatter+body to disk atomically (tmp + rename, echo-marked), then upsert notes/note_tags/note_links and append a versions row in one SQLite transaction; path traversal blocked by resolveVaultPath (lib/storage/local.ts:135-145); every save enqueues a best-effort git auto-commit.StorageAdapter.createNote/updateNote/..., app/_actions/notes.tslocal
Note CRUD via RPCs (saas)notes_save(...) (SECURITY INVOKER) updates the note row and appends a versions row atomically with OCC raising SQLSTATE 40001 on mismatch (lib/db/migrations/pg/0006_notes_save.sql:46-49); notes_rename(...) rewrites [[wikilinks]] across the workspace in SQL.lib/storage/supabase/writes.tssaas
Conflict auto-snapshotStale SaveOpts.expectedVersionId snapshots the about-to-be-overwritten body as a versions row (authorKind:'system', message auto-conflict-snapshot), then lets the incoming write win; snapshot id delivered via SaveOpts.onConflictSnapshot. SaaS retries notes_save with p_expected_version_id=null after a 40001 race.lib/storage/adapter.ts:39-68, lib/storage/local/writes.ts:255-277, lib/storage/supabase/writes.ts:100-183both
Soft-delete trash + restoredeleteNote defaults to trash: local moves the file to <vault>/.eli/trash/<path> and stamps trashed_at; SaaS stamps only. trash:false hard-deletes. restoreNoteFromTrash reverses it. NoteFilter.onlyTrashed powers /v/trash.deleteNote(id,{trash}), restoreNoteFromTrashboth
Rename with wikilink rewriteLocal: move file, then rewrite [[oldPath]] / [[oldPath-sans-.md]] / [[oldTitle]] in every other note (fence-aware, alias-preserving) with system versions (lib/storage/local/writes.ts:409-445). SaaS: the same, inside notes_rename SQL.renameNoteboth
Archive / bucketsarchiveNote sets frontmatter status:'archived'. Bucket classification (inbox/active/archived) honours both path prefix and frontmatter status, archived winning; SQL predicates use IFNULL to defeat SQLite NULL logic (lib/storage/helpers/status.ts:47-55).archiveNote/unarchiveNote, bucketCounts, NoteFilter.bucketboth
Listing & filteringlistNotes supports type, status, tag, bucket, projectId, substring query, folder prefix, favorite, onlyTrashed, limit (1-500 local / 200 saas) and updated_at cursor pagination; getNote accepts ULID/UUID id or vault-relative path.listNotes(filter,opts), getNote(idOrPath)both
Full-text searchLocal: contentless FTS5 notes_fts kept in sync by AFTER triggers (lib/db/migrations/sqlite/0001_fts.sql), sanitized per-token prefix matching, bm25 ranking, <mark> snippets. SaaS: notes_search RPC over a stored generated tsvector, websearch_to_tsquery + ts_rank_cd, SECURITY INVOKER so RLS applies (0010_notes_search.sql).StorageAdapter.search()both
Embeddings storageLocal: note_embeddings Float32Array BLOBs keyed by model+dim, with bulk upsert, missing-list, progress counts, brute-force cosine KNN (lib/storage/local/embeddings.ts). SaaS: notes.embedding vector(1536) + HNSW index, service-role writes, notes_knn RPC under RLS. Note: StorageAdapter.semanticSearch itself still throws — C3 calls these modules directly.knnByEmbedding, notes_knn RPC, scripts/embed-backfill.mjsboth
BacklinksLocal: note_links matched against target id, path, path-sans-.md, and title, with source line + surrounding text (lib/storage/local/reads.ts:334-387). SaaS: by target_id with relational select.backlinks(id)both
Sidebar lensesProjects (type:'project' notes + child counts), Types (notes-as-source-of-truth: type:'type' notes contribute _icon/_color/_order/...), hierarchical Tags via /, Folders derived from paths with recursive counts.listProjects/listTypes/listTags/listFoldersboth
Versions & section diffAppend-only versions rows on every write (authorKind user/agent/clipper/system, parent chain); listVersions ordered by createdAt then monotonic ULID; diffVersions segments bodies into heading sections with stable slug anchors + diff-match-patch hunks (lib/storage/helpers/section-diff.ts:166-267); replaceSection for per-section restore. Consumed by C6.listVersions/getVersion/restoreVersion/diffVersionsboth
Comments (anchors, orphans, reattach)Comments anchor to heading-path slugs or opaque block:<ulid> anchors; flagOrphanedComments stamps orphaned_at when a heading anchor no longer resolves; reattachComment clears it (SaaS also writes a comment.reattached audit row).listComments/addComment/.../reattachCommentboth
FavoritesLocal: vault-relative paths stored in config.jsonso starring never dirties a note's content hash (lib/config/schema.ts:9-16). SaaS: user_favoritesjoin table with strictly self-only RLS — admins cannot see others' stars (0009_user_favorites.sql:31-37).listFavorites/isFavorite/toggleFavoriteboth
AttachmentsSaaS only: upload to private attachments bucket at <workspace_id>/<note_id>/<attachment_id>-<name>, row insert with orphan-object cleanup, 1-hour signed URLs; bucket RLS ties folder segment to membership (lib/storage/supabase/attachments.ts:23-125). Local throws NotImplementedError.putAttachment/getAttachmentURLsaas
Public share linksSaaS only: sha256(token) at rest, plaintext returned exactly once; resolveShareLink uses the service-role client (token possession is the authorisation) and returns uniform null to prevent enumeration (lib/storage/supabase/shares.ts:113-146); notes_select_share RLS policy honours a server-minted share_note_id JWT claim.createShareLink/resolveShareLinksaas
Templates & daily noteapplyTemplate (local) reads <vault>/.eli/templates/<id>.md through the shared template engine; SaaS throws (Phase 6). todayNote gets-or-creates daily/YYYY-MM-DD.md. See C8.applyTemplate/todayNoteboth
Live updates: watcher → index → SSEinstrumentation.ts starts a chokidar watcher on the active vault at boot; .md add/change/unlink triggers lib/index/incremental.updatePath then emits a ChangeEvent on the global bus; /api/events streams it as SSE (25s heartbeats, no backlog replay). Adapter writes markEcho() a 750ms per-path suppression. Delivery view in C9.instrumentation.ts, GET /api/events, lib/watch/events-bus.tslocal
Vault scanning & indexingscanVault walks all .md files (skipping .git/.eli/node_modules/.next), parses frontmatter/tags/wikilinks/title, skips unchanged files by sha256 hash, batches 500-row transactions, abortable, prunes deleted rows; first-touch scan gated by an exclusive-transaction meta row so concurrent Next workers don't race (lib/storage/local.ts:87-128).scanVault, app/_actions/vault.ts, /api/dev/rescanlocal
Local config storeZod-validated, mtime-cached, atomically-written (0600) ~/.eli/config.json: vault registry + activeVaultId, per-vault favorites, auto-generated localToken/mcpToken, aiKey (BYOK), git auto-commit config, browser-extension pairings, keybinding overrides, aiGateway routing. ELI_HOME overrides ~/.eli.lib/config readConfig/updateConfiglocal
SQLite index client & migrationsopenVaultIndex opens ~/.eli/index/<sha256(vaultPath)[:16]>.db with WAL pragmas, runs lexicographic idempotent .sql migrations, caches clients on globalThis with inode-change staleness detection (lib/db/client.sqlite.ts:56-99).openVaultIndex/closeVaultIndex/closeAlllocal
Postgres schema, RLS & tenancyHand-written canonical DDL (16 migrations): workspaces, memberships (5-role ladder), profiles, notes (generated fts tsvector, embedding vector(1536)), versions, note_links, comments, attachments, share_links, pending_invites, audit_log, PATs, user_favorites, ai_keys, chat threads, ai_usage; DB-level triggers bootstrap tenancy and audit. Consumed heavily by C7.supabase db push (CI), lib/db/migrations/pg/runner.ts (PGlite tests)saas
AI BYOK key vaultai_keys_upsert RPC encrypts with pgp_sym_encrypt (aes256) under ELI_KMS_KEY; list returns masked previews; decrypt is service-role-only for immediate forwarding to the AI gateway (C4). Local counterpart: config.json#aiKey.upsertAiKey/.../getAiKeyPlaintextsaas
Audit log & profileslistAuditLog (owner/admin-gated) with batch actor/target name resolution; getProfiles/listWorkspaceMembers power comment avatars and @-mentions.lib/storage/supabase/audit.ts, profiles.tssaas
Test/reference adaptersMemoryAdapter: Map-backed full implementation incl. conflict snapshots and per-channel subscribe. JsonFileAdapter: everything in one atomic-flushed JSON file (schemaVersion 1), documented ~1k-note ceiling. seedSharedWorkspace seeds an owner+viewer fixture for PGlite and real Supabase.lib/storage/memory.ts, lib/storage/json-file.ts, lib/db/seed.pg.tsboth
Bootstrap & middlewareenv.ts validates all env at import (feature flags incl. ELI_LEGACY_EDITOR, ELI_HIPAA, ELI_KMS_KEY, CRON_SECRET); middleware.ts is a no-op unless saas, else refreshes the Supabase session cookie and redirects unauthenticated /w/*, /settings, /onboarding to /login?next=; next.config.ts marks better-sqlite3 + chokidar serverExternalPackages; supabase/config.toml pins the local Supabase stack.env.ts, middleware.ts, instrumentation.tsboth

System view — simplified

System view — simplified

This cluster IS the persistence layer: one interface, two production backends, one mode switch. Standalone, it is a complete local-first markdown note store — point LocalAdapter at any folder of .md files and you get CRUD, full-text search, versions, backlinks, tags, and live re-indexing of external edits, with zero network, zero auth, and an index that can be deleted and rebuilt from the files at any time. The SaaS half is a drop-in replacement of the same interface where Postgres rows are the source of truth and row-level security (not application code) enforces tenancy. Everything above this layer — editor, search UI, AI, agents — is written against the StorageAdapter contract and does not know which backend it is talking to.

System view — detailed

System view — detailed
ComponentRoleFiles
StorageAdapter contractInterface every backend implements: lenses, notes, favorites, search, backlinks/graph, versions, comments, attachments, shares, subscribe, templates; carries SaveOpts (authorKind/agentName/message/expectedVersionId/onConflictSnapshot) and NotImplementedErrorlib/storage/adapter.ts
getStorage mode selectorserver-only factory: ELI_MODE=local LocalAdapter.forVault(ELI_VAULT ?? activeVault.path); else SupabaseAdapter.forActiveWorkspace(). The only sanctioned entry point (ARCHITECTURE.md §2.2)lib/storage/index.ts
LocalAdapterFS + SQLite implementation; per-vault instance cache on globalThis; db() lazily opens the index and runs a serialized first-touch scan; resolveVaultPath blocks escapes; delegates to lazily-imported submoduleslib/storage/local.ts, lib/storage/local/{reads,writes,search,versions,comments,favorites,embeddings,row}.ts
SupabaseAdapterPostgres implementation via user-scoped supabase-js so RLS gates everything; constructed per-request from session + eli_ws cookie slug; fromClient() test seam; resolveShareLink is the only service-role path inside the adapterlib/storage/supabase.ts, lib/storage/supabase/{queries,writes,row,comments,favorites,shares,attachments,ai-keys,knn,embeddings,audit,profiles}.ts
Memory & JsonFile adaptersMap-backed test double (full contract incl. subscribe); single-JSON-file reference backend per ADR-0004, O(N) per op, ~1k-note ceilinglib/storage/memory.ts, lib/storage/json-file.ts
Markdown/domain helpersPure functions shared by all adapters and the scanner: lossless frontmatter round-trip, sha256 hash, tag extraction + ancestor expansion, fence-aware wikilink extract/rewrite, title derivation, bucket classification, section segmenter + slug anchors, orphan scan, section-level diff, per-section replacelib/storage/helpers/{frontmatter,hash,tags,wikilinks,title,status,anchors,orphans,section-diff,section-replace}.ts
SQLite schema & clientDrizzle schema (notes, note_tags, note_links, versions, comments, meta, chat, note_embeddings) + client factory with per-vault globalThis cache, inode staleness detection, WAL pragmas, idempotent lexicographic migration runnerlib/db/schema.sqlite.ts, lib/db/client.sqlite.ts, lib/db/migrations/sqlite/*
Postgres schema, migrations & seedDrizzle type-surface mirroring hand-written canonical SQL (tsvector/vector/bytea customTypes); 16 migrations from base tables through RLS (427 lines), triggers, RPCs, buckets, PATs, ai_keys, knn, chat, ai_usage; PGlite runtime runner for tests; owner+viewer seed fixturelib/db/schema.pg.ts, lib/db/client.pg.ts, lib/db/seed.pg.ts, lib/db/migrations/pg/*
Domain ids & typesBranded string ids minted by a monotonic ULID factory (sub-millisecond ordering for versions); as*() cast helpers for trust boundaries; all domain types (Note, Version, Comment, NoteFilter, ChangeEvent, ...)lib/domain/ids.ts, lib/domain/types.ts
Index pipelineAsync-generator walker (SKIP_DIRS .git/.eli/node_modules/.next); full-vault scanner with hash-skip/batch/progress/abort/prune; per-file incremental reaction (row delete on unlink, else prune:false rescan)lib/index/walk.ts, lib/index/scanner.ts, lib/index/incremental.ts
Watcher & events busPer-vault globalThis watcher registry, awaitWriteFinish, .md-only, 750ms echo suppression, calls updatePath then emits ChangeEvent; bus is a globalThis singleton pub/sub with a 256-event ring bufferlib/watch/chokidar.ts, lib/watch/events-bus.ts
Config store~/.eli/config.json (ELI_HOME-overridable) read/repair/write with mtime cache and 0600 atomic writes; vault registry, tokens, favorites, git, pairings, keybindings, aiGateway, aiKeylib/config/{index,schema,path}.ts
Bootstrap & mode plumbingZod-validated env; boot-time watcher start (Node runtime, local only); SaaS session refresh + auth redirects; serverExternalPackages for native deps; drizzle configs; pinned local Supabase stackenv.ts, instrumentation.ts, middleware.ts, next.config.ts, drizzle.config.ts, drizzle.config.pg.ts, supabase/config.toml

Data flow — simplified

Data flow — simplified

The single most load-bearing flow: a local save writes the markdown file first (disk is the source of truth), then updates the derived SQLite index and appends an immutable version row in one transaction. markEcho is stamped before and after the rename so the chokidar watcher discards the filesystem event the write itself produces — without it every save would trigger a redundant full rescan. The emitted ChangeEvent fans out over SSE so other open tabs refresh.

Data flow — detailed

Local save (file-first write, echo suppression) followed by an external edit picked up by the watcher:

Data flow — detailed: local file-first save with echo suppression, then an external edit picked up by the watcher

SaaS save via notes_save RPC with optimistic concurrency and conflict snapshot:

Data flow — detailed: SaaS save via the notes_save RPC with optimistic concurrency and conflict snapshot

Works separately / works together

Standalone

With every other cluster turned off, this cluster is still a usable product core: a local markdown vault with CRUD, atomic file-first saves, full-text search (FTS5), version history with section-level diff, backlinks, tag/type/folder lenses, trash/restore, and an index that self-heals — delete ~/.eli/index and the first db() call rebuilds it from the files (lib/storage/local.ts:87-128). External tools (Obsidian, scripts, agents editing files directly) are first-class: the watcher re-indexes their writes within milliseconds. The SaaS half standalone is a complete multi-tenant note database whose invariants (role ladder, append-only versions, tenancy bootstrap, audit) live in the database itself — triggers and RLS hold even if application code is bypassed. MemoryAdapter and JsonFileAdapter prove the contract is backend-agnostic.

Composed

  • Every cluster above consumes this one exclusively through getStorage()app/_actions/*.ts and all app/(local)/v/* + app/(saas)/(app)/w/* RSC pages (C2), MCP tools (C5), the clipper app/api/clip/route.ts (C8), and cron routes (C9).
  • C3 Semantic search: lib/search/hybrid.ts and app/api/search/hybrid/route.ts bypass adapter.semanticSearch (which throws) and call lib/storage/local/embeddings.knnByEmbedding / lib/storage/supabase/knn.knnNotesdirectly, reusing the adapter's RLS-scoped sb client; app/api/ai/embed/route.ts, lib/ai/auto-embed-queue.ts, and scripts/embed-backfill.mjs populate the embedding stores.
  • C4 AI platform: lib/ai/key-resolver.ts reads BYOK keys from lib/storage/supabase/ai-keys.getAiKeyPlaintext (service-role decrypt) or config.json#aiKey; workspaces.ai_gateway_config and config.json#aiGateway feed gateway routing; chat_threads/chat_messages/ai_usage exist in both schemas so the chat UI is mode-agnostic.
  • C5 Agent surface: MCP servers (bin/eli-mcp.mjs stdio, app/api/mcp/[transport]/route.ts HTTP) authenticate against config.json#mcpToken (local) or personal_access_tokens rows (saas) and operate on notes purely through the adapter contract; comment section anchors power the add_comment tool.
  • C6 Versioning: the versions tables, diffVersions section diff, and replaceSection written here are the entire data layer of the history UI; every local save additionally calls lib/git/commit-loop.enqueueCommit using config.json#git.
  • C7 SaaS collaboration: memberships/invites/audit/profiles/shares/attachments and all RLS policies defined in lib/db/migrations/pg/ are its foundation; middleware.ts is its request-level gate.
  • C9 Platform ops: owns the delivery side of the freshness machinery described here (SSE route, VaultEventsBridge, kill switches), the dev/test endpoints that reach into closeAll/_resetCacheForTests/scanVault, the pgTAP CI gate over the RLS migrations, and the perf gate over local search.
  • Frontend live updates: app/api/events/route.ts subscribes to lib/watch/events-bus and streams ChangeEvents to components/vault-events-bridge.tsx, which debounces 500ms and calls router.refresh().

Load-bearing details

  • ELI_MODE is a zod enum ['local','saas'] defaulting to 'local' (env.ts:15); saas additionally requires NEXT_PUBLIC_SUPABASE_URL/ANON_KEY via superRefine (env.ts:103-120). Verified in source.
  • The local index DB lives at ~/.eli/index/<first-16-hex-of-sha256(vaultPath)>.db (lib/db/client.sqlite.ts:31-34, verified); ELI_HOME overrides ~/.eli (lib/config/path.ts:10-12).
  • SQLite clients, LocalAdapter instances, watchers, and the event bus are all cached on globalThis (__ELI_SQLITE_CACHE__, __ELI_LOCAL_ADAPTERS__, __ELI_WATCHERS__, __ELI_EVENT_BUS__) to survive Turbopack multi-bundle evaluation and dev HMR; the SQLite cache is additionally invalidated by inode change so a wiped/recreated DB file under a live dev server isn't read through a stale fd (lib/db/client.sqlite.ts:19-29,62-73).
  • The two hash schemes are not comparable across modes: local content_hash = sha256 of the full serialized file (frontmatter block + body, lib/storage/helpers/hash.ts); SaaS/memory/json-file compute sha256(body + JSON.stringify(frontmatter)) (lib/storage/supabase/writes.ts:40,98; memory.ts:72).
  • Conflict handling is last-write-wins + snapshot, never a 409: a stale expectedVersionId snapshots the loser to a versions row tagged auto-conflict-snapshot and proceeds; onConflictSnapshot fires at most once per updateNote (lib/storage/adapter.ts:43-68; local/writes.ts:255-277; supabase/writes.ts:100-183, verified). notes_save raises errcode 40001on OCC mismatch and the adapter's retry force-accepts with p_expected_version_id=null after snapshotting (0006_notes_save.sql:48-49, verified; supabase/writes.ts:132-172, verified).
  • Version author identity diverges: local always stores author_id NULL (local/writes.ts:168); SaaS notes_save derives it from request.jwt.claims.sub inside the function (0006:38); local comments use the literal sentinel 'local' as author_id (local/comments.ts:23).
  • IDs are monotonic ULIDs locally (sub-millisecond ordering for versions, lib/domain/ids.ts:1-9, verified) but random UUIDs in SaaS; getNote disambiguates id-vs-path by ULID regex locally (local/row.ts:36-38) and UUID regex in SaaS (supabase/queries.ts:124).
  • Echo suppression: markEchostamps a 750ms-TTL per-relative-path entry, GC'd beyond 500 entries; an entry is consumed (deleted) on first match (lib/watch/chokidar.ts:36-59, verified). atomicWrite marks before and after the rename because chokidar's awaitWriteFinish can fire later than the rename returns (local/writes.ts:35-49, verified).
  • incremental.updatePath deliberately re-runs the full scanVault(prune:false) for any single-file change, relying on sha256 hash-skip for cheapness — an explicitly documented v1 tradeoff (lib/index/incremental.ts:35-42, verified in source comments).
  • First-touch indexing is race-proof across concurrent Next workers: an exclusive transaction stamps meta.initial_scan_at='pending' before scanning (lib/storage/local.ts:100-124).
  • The walker's SKIP_DIRS includes .eli, so files soft-deleted into <vault>/.eli/trash/ never re-index (lib/index/walk.ts:5, verified).
  • Postgres notes.fts is a STORED generated tsvector with a GIN index and notes.embedding is vector(1536) with an HNSW vector_cosine_ops index — declared only in hand-written SQL because drizzle-kit cannot emit them (0000_initial.sql:75-105; schema.pg.ts:34-41). notes_knn returns 1 - cosine distance to match the local knnByEmbedding contract (0013_notes_knn.sql:31-36); local embedding BLOBs use explicit little-endian DataView reads for cross-CPU portability (local/embeddings.ts:45-53).
  • RLS role ladder: notes/versions/note_links/attachments/share_links writes require owner|admin|editor; comment inserts require commenter+ AND author_id=auth.uid(); versions are append-only (no UPDATE/DELETE policies); user_favorites and chat_threads are strictly self-only even for admins (0001_rls.sql; 0009; 0014). Service-role usage is a documented allow-list (profiles trigger, share-token validation, seeds, invite acceptance, PAT last_used_at bumps, embedding writes — 0001_rls.sql footer).
  • SaaS FTS goes through the notes_search SECURITY INVOKER function specifically to fix a previously injection-prone websearch_to_tsquery string interpolation (0010_notes_search.sql header).
  • postgres-js is intentionally not a dependency: lib/db/client.pg.ts only exposes a lazy factory; the production SaaS runtime path is entirely supabase-js so RLS is enforced via the session JWT (client.pg.ts:4-19). The pg migration runtime runner is used only by PGlite tests; production applies migrations via supabase db push (lib/db/migrations/pg/runner.ts:12-15).
  • config.json is written 0600 (dir 0700); localToken (CSRF/DNS-rebinding cookie) and mcpToken are auto-generated 32-byte base64url secrets on first read (lib/config/index.ts:19-47); forward-compatible repair merges user data over defaults on schema mismatch (index.ts:103-115).
  • instrumentation.register() only runs when NEXT_RUNTIME==='nodejs' and ELI_MODE is unset-or-local, because chokidar and better-sqlite3 break Edge bundles (instrumentation.ts:16-17); both are serverExternalPackages (next.config.ts:7).
  • ai_keys ciphertext uses pgp_sym_encrypt with cipher-algo=aes256, s2k-mode=3, s2k-digest-algo=sha256 keyed by ELI_KMS_KEY (min 16 chars, env.ts:45); decrypt is a service-role-only RPC (0011_ai_keys.sql:157,170-186).
  • Bucket predicates wrap status in IFNULL(...,'') to avoid SQLite 3-valued NULL propagation silently hiding status-less notes (lib/storage/helpers/status.ts:41-55); default listNotes excludes archived but includes inbox; bucket:'all' disables status filtering (local/reads.ts:179-190).

Gaps and partially wired

  • StorageAdapter.semanticSearch throws NotImplementedError in both production adapters (lib/storage/local.ts:244-246; supabase.ts:133-135, both verified) even though the underlying embedding stores are fully built — hybrid search (C3) consumes lib/storage/local/embeddings.ts and lib/storage/supabase/knn.ts directly, including reaching into SupabaseAdapter.sb as a public field. The contract method is effectively dead.
  • graph() throws everywhere — reserved for Phase 7 (graph view).
  • subscribe is a no-op in both LocalAdapter and SupabaseAdapter (local.ts:315-321; supabase.ts:295-299): local live updates flow watcher → bus → SSE instead; SaaS has no real-time channel at all pending Supabase Realtime. Only Memory/JsonFile implement in-process channel subscription.
  • Local mode has no attachments and no share links (local.ts:299-304 and share methods throw NotImplementedError).
  • SaaS applyTemplate throws (Phase 6) — templates are local-only today (C8).
  • JsonFileAdapter is deliberately partial: comments, attachments, shares, and diff throw; O(N) per operation with a documented ~1k-note ceiling (lib/storage/json-file.ts:43-78).
  • Incremental indexing is not incremental: any single-file watcher event triggers a full-vault scanVault(prune:false) walk, cheap only because of hash-skip (lib/index/incremental.ts:35-42) — acknowledged in-source as a v1 tradeoff to optimize when chokidar fan-out becomes a bottleneck.
  • notes.locked_by is a reserved column in the Postgres schema with no wiring.
  • Cross-mode hash incompatibility: local vs SaaS content_hash schemes differ (see load-bearing details), so hashes cannot be used to compare or sync content across modes.
  • recentEvents() ring buffer is dead to the SSE path: /api/events intentionally does not replay backlog on connect (replay caused router.refresh flurries that broke e2e), and the companion hooks/use-vault-events.ts hook is currently unmounted — see C9.
  • No Supabase adapter-contract test run exists: tests/_helpers/pglite.ts docs reference a tests/adapter/supabase.spec.ts that is not in the tree; the contract suite runs only against Local/Memory/JsonFile (C9).
  • CRON_SECRET is zod-optional (env.ts:98) — if a deployment forgets to set it, both cron endpoints permanently return 401 (fail-closed, but silently disables digests and trash pruning; C9).