backend / selah

Selah data and agent layer

A local SQLite data layer for a terminal Bible: importers that build FTS5-indexed text, commentary and Hebrew/Greek concordance databases, a reading tracker, and one set of read-only tools served to Claude, Codex and any MCP client.

2026 · live · open source

tui → claude: AG-UI event streamtui → codex: spawn, JSONL eventsclaude → tools: mcp__selah__* in-processcodex → mcp: MCP over stdioextmcp → mcp: MCP over stdiomcp → tools: callTooltools → bibledb: passages, FTS5 bm25tools → studydb: commentary, concordancetools → userdb: notes, reading contexttui → tracker: key/scroll activitytracker → userdb: sessions, INSERT OR IGNORE readsimporter → github: HTTPS, retries + verifyimporter → bibledb: transactional replace + FTS rebuildimporter → studydb: staging tables + rename swapClaude Code / Codex /MCP clientClaude Code / Codex / MCP client (client) external agentsSelah TUI (Ask tab)Selah TUI (Ask tab) (client) OpenTUI + SolidImportersImporters (worker) retrying download, staged swapClaude Agent SDKClaude Agent SDK (model) in-process SDK MCP servercodex exec (read-only)codex exec (read-only) (model) ephemeral subprocessReading trackerReading tracker (service) active time, read rulesselah mcpselah mcp (service) stdio MCP server, 9 toolsTool layerTool layer (service) zod schemas, 7k output budgetbible.dbbible.db (store) verses + FTS5study.dbstudy.db (store) commentary FTS5, concordanceuser.dbuser.db (store) sessions, reads, notesGitHub raw / codeloadGitHub raw / codeload (third party) commit-pinned sourcesAG-UI event streamspawn, JSONL eventsmcp__selah__* in-proc…MCP over stdioMCP over stdiocallToolpassages, FTS5 bm25commentary, concordan…notes, reading contextkey/scroll activitysessions, INSERT OR I…HTTPS, retries + veri…transactional replace…staging tables + rena… tui → claude: AG-UI event streamtui → codex: spawn, JSONL eventsclaude → tools: mcp__selah__* in-processcodex → mcp: MCP over stdioextmcp → mcp: MCP over stdiomcp → tools: callTooltools → bibledb: passages, FTS5 bm25tools → studydb: commentary, concordancetools → userdb: notes, reading contexttui → tracker: key/scroll activitytracker → userdb: sessions, INSERT OR IGNORE readsimporter → github: HTTPS, retries + verifyimporter → bibledb: transactional replace + FTS rebuildimporter → studydb: staging tables + rename swapClaude Code / Codex/ MCP clientClaude Code / Codex / MCP client (client) external agentsSelah TUI (Ask tab)Selah TUI (Ask tab) (client) OpenTUI + SolidImportersImporters (worker) retrying download, staged swapClaude Agent SDKClaude Agent SDK (model) in-process SDK MCP servercodex exec(read-only)codex exec (read-only) (model) ephemeral subprocessReading trackerReading tracker (service) active time, read rulesselah mcpselah mcp (service) stdio MCP server, 9 toolsTool layerTool layer (service) zod schemas, 7k output budgetbible.dbbible.db (store) verses + FTS5study.dbstudy.db (store) commentary FTS5, concordanceuser.dbuser.db (store) sessions, reads, notesGitHub raw /codeloadGitHub raw / codeload (third party) commit-pinned sourcesAG-UI event streamspawn, JSONL eventsmcp__selah__* in-proc…MCP over stdioMCP over stdiocallToolpassages, FTS5 bm25commentary, concordan…notes, reading contextkey/scroll activitysessions, INSERT OR I…HTTPS, retries + veri…transactional replace…staging tables + rena…
clientmodelserviceworkerstorethird partyasync
  • [*]LanguagesTypeScript
  • [*]FrameworksBun · Model Context Protocol SDK · Claude Agent SDK · AG-UI event protocol · zod · OpenTUI + Solid (client)
  • [*]StoresSQLite (bun:sqlite, WAL) · SQLite FTS5
  • [*]AsyncAsync-generator agent event streams (AG-UI) · Batched imports yielding to the event loop
  • [*]Infrabun build --compile (8 release targets + SHA256SUMS) · Cloudflare Pages (site) · AUR package · GitHub Releases
  • [*]Third partiesGitHub raw/codeload (scrollmapper bible_databases, lyteword mhenry-complete, STEPBible-Data) · GitHub Releases API (site build) · Claude via Claude Agent SDK · OpenAI Codex CLI

Selah is a terminal Bible reader, and the backend is everything below the UI: about 4,000 lines in src/data, src/agent, src/mcp.ts and src/cli.ts. Data is split across three SQLite files. bible.db holds public-domain translations with an FTS5 index, study.db holds Matthew Henry's commentary (its own FTS5 index) and a STEPBible word-level concordance, and user.db holds sessions, reads, bookmarks, highlights, notes and Ask threads, so re-importing or deleting a translation can never touch a reader's history.

Every source is pinned to a git commit and downloaded with a retrying fetcher: 4 attempts, backoff of 2s/6s/15s, Retry-After honoured, a 60s no-bytes idle cut, and a verify hook that rejects a truncated or wrong file before it is imported. Caches are written to a temp file then renamed. A translation import replaces its rows in one transaction and rebuilds the FTS index; the concordance import builds staging tables under a generation letter in batches, yielding to the UI between them, then swaps them in with a transactional rename so live queries are never served half an import.

The reading tracker counts only active time (gaps over 3 minutes count as zero) and marks a chapter read only when its end was reached and active time passed a word-count threshold, with one row per chapter per day enforced by a primary key and INSERT OR IGNORE.

The Ask agent and outside agents share nine tool definitions with zod schemas and a 7,000-character output budget. Claude runs them through the Agent SDK's in-process MCP server with Bash, file and edit tools disallowed; Codex is spawned as codex exec --ephemeral -s read-only and reaches the same tools through selah mcp over stdio. 31 test files with 318 cases cover the data layer, tools, MCP server, downloads and UI.

How it holds up

Auth
No server-side auth: it is a local-first app, and the agents run on the reader's own Claude Code or Codex login; access control is tool scoping instead: Claude gets only mcp__selah__*, WebSearch and WebFetch with Bash/Read/Write/Edit/Glob/Grep disallowed, Codex runs ephemeral in its read-only sandbox from a scratch directory with its shell tools turned off, and every MCP tool is annotated readOnlyHint.
Data
16 tables in three SQLite files: bible.db (books, translations, verses, chapter_stats + verses_fts), study.db (commentary_sections, commentary_paragraphs, words, lexicon + commentary_fts) and user.db (settings, sessions, chapter_reads, bookmarks, highlights, ask_threads, journal, notes). Two FTS5 indexes, both external-content with porter unicode61 remove_diacritics tokenisation; schemas are CREATE IF NOT EXISTS with no versioned migrations.
Consistency
Translation imports delete and reinsert a translation's rows inside one transaction, then rebuild the FTS index; the concordance builds generation-lettered staging tables and swaps them in with DROP + ALTER TABLE RENAME in one transaction; reads are idempotent per chapter per day via a composite primary key and INSERT OR IGNORE; personal writes use ON CONFLICT upserts.
Failure
Downloads retry on a fixed set of retryable statuses (408, 425, 429, 5xx including Cloudflare 52x) with backoff and Retry-After, abort an attempt after 60s without bytes, and treat a failed verify as a retryable attempt; corrupt cached sources are deleted and refetched; temp-then-rename writes prevent half-written caches; a failed concordance import leaves the live tables untouched; tool errors return isError rather than throwing into the agent.
Testing
31 test files (test/*.test.ts[x]) with 318 test() cases, run by bun test against a shared imported KJV + BSB database; includes tracker (9), MCP (15), download (11), concordance (23), agent, Codex, commentary, install and UI frame-capture tests.

Techniques

  1. Generation-swapped concordance importThe STEPBible import builds words/lexicon tables and their indexes under the other generation letter in batches, yielding to the UI between them, then swaps them in with DROP + RENAME in one transaction, so readers never see a partial import and a failure leaves the live tables intact.src/data/concordance.ts:221 src/data/concordance.ts:227 src/data/concordance.ts:269
  2. Transactional translation import with external-content FTS5Each translation's verses and chapter word counts are replaced in one transaction, then the external-content FTS5 index is rebuilt; search parses user input into a quoted, injection-safe MATCH expression with in:book/in:ot filters and ranks by bm25 with highlight markers.src/data/importer.ts:76 src/data/importer.ts:105 src/data/db.ts:49 src/data/bible.ts:36 src/data/bible.ts:145
  3. Resilient, verified downloads of commit-pinned sourcesSources are pinned to exact upstream commits; the downloader retries only retryable statuses with backoff and Retry-After, cuts attempts that go silent for 60s, runs a verify hook (66 books, correct chapter counts) and writes via temp file plus rename.src/data/translations.ts:15 src/data/download.ts:35 src/data/importer.ts:37 src/data/importer.ts:45
  4. Honest reading trackerActive time counts only input gaps up to 3 minutes; a chapter counts as read when its end is reached and active time exceeds 30% of a 250-wpm read (clamped 1-30s); the read is credited to the session's start day and stored once per chapter per day via a primary key and INSERT OR IGNORE.src/data/tracker.ts:5 src/data/tracker.ts:15 src/data/user.ts:30 src/data/user.ts:212
  5. One read-only tool set for three agent pathsNine zod-typed tools with a 7,000-character output cap are registered once and served to Claude through the Agent SDK's in-process MCP server (shell and file tools disallowed), to Codex as a read-only ephemeral subprocess wired to selah mcp, and to any MCP client over stdio; the agent can only suggest a note, never write one.src/agent/tools.ts:34 src/agent/claude.ts:14 src/agent/claude.ts:44 src/agent/codex.ts:21 src/mcp.ts:16
  6. Personal data isolated from content databasesReader data lives in user.db and content in bible.db/study.db, so removing or re-importing a translation or study pack cannot touch history, notes or streaks.src/data/user.ts:6 src/index.tsx:89 src/data/importer.ts:110

Counted

  • 76Commits
    git rev-list --count HEAD
  • 3 (bible.db, study.db, user.db)SQLite databases
    src/data/paths.ts:38,42,49
  • 16Tables
    grep -c 'CREATE TABLE' src/data/{db,user,commentary,concordance}.ts (4+8+2+2)
  • 2 (verses_fts, commentary_fts)FTS5 indexes
    src/data/db.ts:49, src/data/commentary.ts:36
  • 9MCP / agent tools
    grep -c 'name: "' src/agent/tools.ts
  • 31Test files
    git ls-files test | grep -cE '\.test\.tsx?$'
  • 318Test cases
    grep -cE '^\s*(test|it)(\.each)?\(' over the 31 test files, summed
  • 4,017Backend LOC (data, agent, mcp, cli)
    git ls-files 'src/data/*.ts' 'src/agent/*.ts' src/mcp.ts src/cli.ts | xargs wc -l | tail -1
  • 10,696All src LOC excl. theme JSON and scenes.json
    git ls-files src | grep -v -e theme/assets -e scenes.json | xargs wc -l | tail -1
  • 8Release targets
    scripts/build.ts:14 RELEASE_TARGETS
  • 4 / 2s, 6s, 15sDownload attempts / backoff
    src/data/download.ts:48