Infrastructure
AI Gospel Library
The problem this solves
Section titled “The problem this solves”Every existing scripture app I have used treats verses as static text. I read, I close the app, nothing carries over. Real scripture study is not consumption — it is annotation, cross-referencing, attaching the conference talk that unlocked a verse, tagging a theme that shows up in three different books, dropping a pin on the place a story happened. None of that is supported by the official Gospel Library app, and even the third-party tools I tried treat the verse as a destination instead of an object.
I wanted my own. Local-first so it works on a plane. Cross-canon so a tag in 1 Nephi and a tag in Romans can connect. Multimedia so I can pin the General Conference talk that explained a passage. Mine.
What it is
Section titled “What it is”A Next.js app deployed at gospel-library.jameshurst.com covering the full LDS canon — Book of Mormon, Old Testament, New Testament, Doctrine and Covenants, Pearl of Great Price. Every verse is an interactive object you can:
- Annotate with rich-text notes (single verse or verse range)
- Tag with a custom color-coded label
- Cross-reference to any other verse or range
- Attach multimedia to (YouTube videos, General Conference talks, images, hymns, generic links)
- Geographically locate (Leaflet map, supports both real Holy Land geography and theoretical Book of Mormon geography)
A “Study Journal” view rolls up everything I have annotated by chapter and book. A “My Notes” view surfaces every note I have written. Search is across the whole canon and across my own annotations.
How it works
Section titled “How it works”Four pieces I want to call out, because each one was a deliberate choice:
1. Local-first storage via IndexedDB and Dexie
Section titled “1. Local-first storage via IndexedDB and Dexie”The primary storage for every annotation, tag, cross-reference, and attachment is IndexedDB on the device, wrapped by Dexie. The app loads, reads from IndexedDB, and renders — no network required. Sign-in is optional. If I never sign in, my annotations live on the device only.
This was the design that came after I tried the other way first. A cloud-first build means a flaky network kills the experience the moment I am on a plane or driving through a canyon. Dexie made local-first easy: the same query API I would write against Supabase works against IndexedDB, just synchronous and zero-latency.
When I do sign in, Supabase becomes the sync target and the multi-device replicator. The local store stays authoritative; the cloud follows.
2. Self-hosted Supabase — a second instance, deliberately isolated
Section titled “2. Self-hosted Supabase — a second instance, deliberately isolated”The cloud sync layer is Supabase. But not Supabase Cloud and not the same self-hosted instance that runs my main projects.
The VPS already runs a self-hosted Supabase stack at db.jameshurst.com, used by my main projects. When I started building Gospel Library, I considered just adding a schema to that instance. I did not. Two reasons:
- Isolation. A runaway query or a misbehaving migration on Gospel Library should not be able to take down my main data plane. Two stacks, two ports, two Caddy routes. If one falls over, the other keeps running.
- Independent versioning. Supabase ships fast. Sometimes I want to pin Gospel Library to a stable Supabase release while letting the main instance ride a newer one for a feature I need elsewhere. Easy when they are separate stacks. Painful when they are sharing a database.
So Gospel Library has its own full Supabase stack — Postgres, GoTrue auth, Kong API gateway, PostgREST, Storage, Studio, Realtime, Edge Functions, Logflare analytics, Vector, imgproxy, Supavisor connection pooler. Fourteen containers, all named with the gospel- prefix, mapped to non-conflicting ports. Reachable internally at gospel-db.jameshurst.com for the Studio dashboard, with the public app talking to the Kong gateway on port 8001.
This is not a thing I would set up casually for every project. But for an app I want to live forever and never depend on a cloud vendor for, the second stack is the right boundary.
3. Email auth via Resend
Section titled “3. Email auth via Resend”GoTrue (the Supabase auth service) handles password resets, email verification, and magic links. The actual email delivery is wired to Resend. When I tested the reset flow on April 26, 2026, the email landed in my inbox in under a minute, complete with a six-digit fallback code in case the deep link did not open. No SMTP configuration, no DNS gymnastics — Resend’s API key in the GoTrue env file and that was it.
4. Caddy plus Docker plus auto-deploy
Section titled “4. Caddy plus Docker plus auto-deploy”The web app itself is a standard Next.js production build, packaged as a Docker container, exposed on port 3100, and reverse-proxied through Caddy. Caddy terminates TLS at the public IP, routes gospel-library.jameshurst.com to the container, and auto-renews Let’s Encrypt certs without me touching anything.
Pushes to main trigger an auto-deploy on the VPS via the existing webhook receiver pattern (same one used for the rest of my repos). The VPS pulls, rebuilds the container, restarts.
Why this is not trivial
Section titled “Why this is not trivial”Three non-obvious failure modes had to be solved before this felt right:
Local-first sync is harder than it looks. A naive “save locally, also save to Supabase” pattern produces conflicts the first time you edit on two devices. The current model treats IndexedDB as authoritative and treats Supabase as an eventually-consistent backup that gets reconciled on sign-in. Conflict resolution is “last write wins by timestamp,” which is fine for an app where the only writer is me, but would not be enough for shared annotations down the road.
Two Supabase instances on one VPS is a port and naming exercise. Both stacks want to bind to default ports. Both name their containers things like db, auth, kong, realtime. Renaming everything to gospel- plus the role and remapping every internal port reference took a careful pass through the docker-compose file. Easy to get wrong. Easy to miss a single env var that still references the default name.
Leaflet plus Next.js plus z-index is a dance. The map is great. The map also happily renders above modals, dropdowns, and the user menu unless every container in the stack has explicit z-index and isolation: isolate. Build 368 was specifically the fix where the map stopped overlapping the user dropdown.
Where it is going
Section titled “Where it is going”- AI study tools — ask a question about a passage, get cross-references suggested automatically. Hooks into
claude -p(Max plan, no paid API). - Study session tracking — what did I read, when, and what did I learn? Daily roll-up to surface in the morning briefing.
- Shared annotations — eventually, study with the family and see each other’s notes.
- Integration with
conference-talk-reader— the Chrome extension that highlights conference transcripts word-by-word should know which talks I have already attached to verses. - Mobile-optimized experience — the desktop reader is solid; the phone reader is the next frontier.
The repo is ~/apps/ai-gospel-library on every machine. Build numbers in build.txt. Commits follow the standard Build X: short summary convention.
Related
Section titled “Related”- Repo portfolio entry: Repos
- VPS services map: VPS Services Overview
- Companion repo: conference-talk-reader — the Chrome extension for highlighting Conference transcripts
- Companion repo: scripture-stories — the family Bible study companion