尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Personal Bible study app: reader, cross-references, context, original languages, My links, annotations
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗A local, single-user Bible study app: read the whole Bible, click any verse to open a side drawer with ranked cross-references (readable in context), historical context (dated events, people, places on a map, book introductions), the Hebrew/Greek behind every word with Strong's definitions and a full concordance, your own notes/highlights/tags, and YouTube videos saved to verses and played in-app. Visual link views: a whole-Bible cross-reference arc diagram, a force-directed link graph, a timeline and maps.
python3 ~/bible-study/serve.py
Then open http://localhost:8765. (The command works from any folder.) No dependencies beyond Python 3 (the map tiles and YouTube embeds need internet; everything else is local).
Notes are saved to notes/notes.json on every change (and mirrored in the browser's localStorage as a backup). notes/config.json holds API keys. When you sign in to a profile (below), notes and progress go to that profile instead.
Optional local profiles: sign in with an email and a password to keep your notes, bookmarks, reading position and study history under your name. Everything stays on this computer and no email is ever sent. Signed out, the app works exactly as before ("guest").
python3 ~/bible-study/serve.py --list-users # profiles on this computer
python3 ~/bible-study/serve.py --reset-password [email protected] # forgot your password: set a new one
--reset-password asks for the new password twice (or reads one line with --password-stdin), works while the server is running, and signs that profile out everywhere. Other flags: --data-dir DIR (default notes/), --max-users 50, --allow-host NAME (serve a Host name other than localhost).
What is tracked automatically per profile (or for the guest): chapters opened and read, verses studied, time spent reading, your last position, a daily streak and the last 1,000 study events. Bookmarks, read marks and progress resets are explicit. Two open windows never overwrite each other: notes merge per verse, and progress is sent as events that the server adds up.
Data layout (notes/ unless --data-dir):
| Path | What |
|---|---|
notes.json, guest-library.json | guest notes and guest bookmarks/progress |
users.json | profiles (email, name, password hash; never the password) |
sessions.json | signed-in browsers (only a SHA-256 of each session token) |
profiles/<id>/notes.json, library.json | one profile's notes and bookmarks/progress |
deleted/<id>-<date>/ | a deleted profile's files on your Mac (remove by hand to erase them for good); on the site a deleted profile is erased at once and nothing is kept here |
profiles/<id>/user.json | who owns that folder (email and name, never the password hash) |
*.bak | a daily backup taken before a notes, library or users.json file is overwritten |
If users.json is ever damaged, the server moves it aside as users.json.corrupt-<date> and refuses sign-ins and new profiles (so nobody signs up again into an empty profile) until you restore it: copy users.json.bak (or a repaired .corrupt file) to users.json. The running server picks it up at once, and everyone who was signed in still is.
Security: passwords are hashed with PBKDF2-SHA256 (600,000 rounds; scrypt where Python has it), sign-in is rate limited with a growing lockout, the session cookie is HttpOnly and SameSite=Strict, every write must be same-origin JSON, and the server only answers to localhost / 127.0.0.1. Export a profile as JSON from the Library, or as Obsidian markdown.
Server tests (start their own server on a free port with a temporary data folder): python3 tests/test_server.py.
| Translation | How | Why |
|---|---|---|
| KJV, BSB | bundled, offline | public domain |
| ESV, NLT | fetched per chapter via the official APIs with your free key (Settings ⚙), cached in notes/cache/ | copyrighted; APIs licensed for personal use |
| NIV, NKJV, NASB | read in the app through API.Bible when BS_APIBIBLE_KEY is set (14-day server cache, FUMS view counts); otherwise opens on Bible Gateway | © Biblica, Thomas Nelson, Lockman |
| LSB | one click opens the chapter on read.lsbible.org | copyrighted, no free API |
Keys: ESV at https://api.esv.org/ · NLT at https://api.nlt.to/.
h (or the Bible Lantern wordmark) — a URL without a chapter opens Home rather than the last chapter. Signed in, or as a guest on your own computer, it shows the verse of the day with its Doré engraving, Continue reading (where you left off), your latest notes, highlights and links, your streak and chapters this week, and the whole Bible as a map shaded by what you've read. A hosted site's guest sees the welcome page instead. #43/3 still opens John 3 directly, and Back returns to Home.[[reference]] into your note. Topics that cite the verse are listed underneath.o — word-by-word Hebrew (Leningrad codex) or Greek (all major editions; amber = textual variant between the KJV's Greek and modern editions) with transliteration, gloss, parsed morphology, Strong's number → definition and every occurrence.n — markdown note per verse or chapter, five highlight colours, tags, [[John 3:16]] links that navigate. Export as an Obsidian-ready zip or JSON.a · Graph g · Map m — visual link views. Search / takes references (rom 8:28), words, Strong's numbers (G26) and topic names. ← → chapters, j k verses, ? help.Optional: the same code can also run as a website anyone can sign up to, with the reader on Vercel and the server and your data on Railway. Local use doesn’t change. With none of the variables below set, python3 serve.py behaves exactly as described above.
browser ──► Vercel (team koum-studio, project bible-study): app/ as static files on Vercel’s CDN
└─ /api/* proxied by vercel.json ──► Railway (project bible-study, service api): python3 serve.py
└─ volume /data: profiles, notes, links, highlights, library, settings
GitHub (public repo) ── push to main ──► both redeploy
The live site is https://www.biblelantern.com (the apex biblelantern.com redirects to it), with the server at api-production-7fa2.up.railway.app. The Vercel project also answers on bible-study-seven-blond.vercel.app.
vercel.json proxies /api/* to Railway, so the session cookie, the same-origin checks and the X-BS-Scope header work as they do locally.app/ as well, so its own domain is a working fallback.main and both redeploy on their own: Vercel republishes app/, and Railway rebuilds and restarts the server (with a short pause, because a service with a volume never runs two copies at once).| File | What |
|---|---|
vercel.json | serves app/ with no build step, proxies /api/* to the Railway domain, sets cache and security headers, and redirects /donate and /support to the donation page |
.vercelignore | sends only app/ (without app/mock/ and dotfiles) and vercel.json to Vercel |
railway.json | Railpack build, python3 serve.py, healthcheck /api/health, restart policy Always |
.python-version | Python 3.12 on Railway (the code still runs on the Mac’s Python 3.9) |
requirements.txt | only a comment: nothing needs installing, it only tells Railway’s builder that this is a Python app |
Caching: index.html, js/ and css/ revalidate on every load (cheap 304s, and the modules always come from the same deploy), the JSON under data/ is cached for 5 minutes, the Doré plates for 30 days, and /api/* never.
Setting it up (once)
.gitignore keeps notes/ (your data and keys) and raw/ out of it.main branch into it and name the service api. On the service:
/data;api-production-7fa2.up.railway.app);python3 serve.py, the healthcheck path /api/health and the restart policy “Always”. Railway reads railway.json only for services that still use config as code, so a new service takes these from its settings. Keep one replica: the data lives in files on one volume.vercel.json rewrites /api/* to that Railway domain: change it there if yours differs, then commit and push. Create a project named bible-study from the repo, with the repo root as its root directory. The framework preset (Other), the output directory and the empty build all come from vercel.json, and Vercel needs no variables. Add the custom domain (www, with the apex redirecting to it) under the project’s Domains.Host = the Railway domain, so BS_ALLOWED_HOSTS needs only that domain. BS_ALLOWED_ORIGINS must list every origin a browser uses for the site, or its writes (sign-in included) get 403 “Cross-site request blocked”: https://www.biblelantern.com, https://biblelantern.com and https://bible-study-seven-blond.vercel.app.https://www.biblelantern.com/api/health and https://api-production-7fa2.up.railway.app/api/health both answer {"ok":true}.www.biblelantern.com and biblelantern.com, and choose the widget mode Managed. Paste its site key and secret key into the Railway variables BS_TURNSTILE_SITEKEY and BS_TURNSTILE_SECRET; Railway redeploys, and from then on the sign-up form runs the check (most people see nothing, a few are asked to click). Until both are set, sign-up is open behind the rate limits only, and the server says so when it starts.Railway variables. All are optional locally (with none set, the server behaves exactly as it does on your Mac); with BS_HOSTED set, BS_DATA_DIR is required.
| Variable | On Railway | What |
|---|---|---|
PORT | set by Railway | the port to listen on (default for --port) |
HOST | 0.0.0.0 | listen on every interface (default for --host; locally it is 127.0.0.1) |
BS_DATA_DIR | /data | where profiles, notes and settings live (default for --data-dir): the volume. Required when BS_HOSTED is set: the server refuses to start on the container disk, and refuses when RAILWAY_VOLUME_MOUNT_PATH is set and the directory is not on that mount |
BS_HOSTED | 1 | hosted mode, below |
BS_ALLOWED_HOSTS | api-production-7fa2.up.railway.app | Host names answered besides localhost (comma list; *.up.railway.app patterns work). Behind the Vercel rewrite this is the only Host serve.py sees. Railway’s health check (Host healthcheck.railway.app) is answered without being listed. |
BS_ALLOWED_ORIGINS | https://www.biblelantern.com,https://biblelantern.com,https://bible-study-seven-blond.vercel.app | origins whose writes pass the same-origin check, besides the request’s own host (comma list): every origin a browser uses for the site |
BS_TRUST_PROXY | 1 | take the visitor’s IP from the first X-Forwarded-For hop (for the sign-in and sign-up rate limits and the auth log) and the scheme from X-Forwarded-Proto; never set it locally |
BS_OWNER_EMAILS | your email | comma list of owner addresses. A profile with one of them has the owner rights (ESV/NLT and their keys, Site activity) only while it holds that address’s claim in DATA/owner-claims.json, which the first profile to sign up with it takes |
BS_PROXY_SECRET | a long random string | when a request carries this value in an X-BS-Proxy-Secret header it came through Vercel, and the visitor’s address is read from X-Vercel-Forwarded-For (else X-Real-IP, else X-Forwarded-For), and Turnstile is given the visitor’s address; add the same header to the rewrite in Vercel (Routing Middleware). Without it, the auth log and the sign-in windows see Vercel’s egress address, keyed per email, and Turnstile is given no address |
Hosted mode (BS_HOSTED=1):
DATA/invites.json is no longer read and may be deleted.BS_MAX_USERS), the per-profile quota (BS_PROFILE_QUOTA_MB), the free-space guard (BS_MIN_FREE_MB) and the rate limits (10 sign-up attempts an hour per visitor and email, 100 an hour across the site; a failed bot check counts). Until BS_PROXY_SECRET is set, every visitor reaches the server from one of Vercel’s addresses, so the per-visitor limits are coarse: Turnstile is the real bot gate.BS_APIBIBLE_KEY is set (else they open on Bible Gateway); LSB opens on read.lsbible.org.Secure.BS_OWNER_EMAILS first) and the last owner profile cannot be deleted. An ordinary profile cannot move to an owner address.Preview deployments. Every branch pushed to GitHub gets a Vercel preview (bible-study-git-<branch>-koum-studio.vercel.app). Its /api goes to the production server too, where its origin is not allowed, so a preview reads production as a guest and can’t sign in or save (a sign-in there fails with “Cross-site request blocked”). That is the safe default: never add preview origins to BS_ALLOWED_ORIGINS on production. A staging Railway environment for previews is optional.
Backups. Railway volume backups are turned on for the /data volume (since 2026-10-08). To restore one: open the volume in Railway, choose Backups → Restore on the snapshot you want; Railway stages it as a new volume at /data, and the next deploy applies it. Now and then also keep a copy off Railway: Profile → Export profile downloads your whole profile as one JSON file, which Import profile file brings back.
Monitoring. /api/health answers 200 {"ok":true} (GET or HEAD) only after a successful write to the data directory and a readable users.json; otherwise 503 {"ok":false} with an [error] line in the Railway log (a healthy answer is reused for 5 seconds, so a burst of probes costs the volume one write). While the volume is low on space it still answers 200, with an [alert] line once an hour. Railway’s own health check uses it, and an external uptime check (HEAD is fine) belongs at https://www.biblelantern.com/api/health, which covers Vercel, the rewrite and Railway. Railway’s service logs show every refused request with its reason.
Traffic. Two views, and alerts:
DATA/metrics.json, 90 days) but never traced back to anyone. It is served by GET /api/admin/traffic, which answers 404 to anyone but an owner.[alert] lines. The server writes one to the Railway log (stderr), at most once an hour of each kind, when within the last hour there were more than 30 sign-ups, 100 failed sign-ins, 50 failed bot checks or 20 server errors, or when the volume is low on space. Railway’s own monitors watch CPU, memory, disk and network rather than log text, so: keep a log widget filtered on "[alert]" in the project’s Observability dashboard, add a disk-usage monitor for the /data volume (Pro plan; it emails you), and to be told about the lines themselves, run a log monitor on that filter, such as Railway’s Monirail template, which posts to Slack, Discord or a webhook.Pausing sign-up. Set BS_SIGNUP=closed on the api service: Railway restarts it, and the sign-up form shows the paused message while everyone can still read and sign in. Delete the variable to reopen. Sign-ups also pause on their own when the volume runs low on space (BS_MIN_FREE_MB) or the site holds BS_MAX_USERS profiles. Whatever the reason, GET /api/auth/me says "signup": "closed" with "signupPause" set to "paused", "storage" or "capacity" (null while sign-up is open), Home announces the pause with a Help us expand link, and every refused sign-up gets the same message: “New sign-ups are paused for now while we expand. Reading stays open to everyone.” Site activity shows the same two fields.
The donation link. https://www.biblelantern.com/donate (and /support, the same) is the link to share; the app links to /donate from Home’s footer, the profile dialog, Settings and the Keyboard shortcuts dialog. It is a temporary redirect (307 on Vercel, 302 from serve.py), so browsers never remember the target and it can be re-pointed without changing any link already shared. To point it somewhere else, change the destination of both /donate and /support in vercel.json’s redirects (then commit and push), and set BS_DONATE_URL on the api service, which covers the Railway domain and local use.
Deleting a profile. Profile → Delete profile on the site removes the person’s notes, bookmarks, links, highlights and reading progress and erases the files at once (the dialog says so); copies linger only in the volume’s own backups until those age out. Locally the files move to deleted/<id>-<date>/ until you remove them.
Passwords. To set a new password for someone on the site, open a shell on the api service (railway ssh, or the service’s shell in the dashboard) and run python3 serve.py --reset-password their@email. BS_DATA_DIR points it at the volume, and it works while the server is running.
Bringing your local data over
python3 serve.py), sign in and choose Profile → Export profile. It downloads one JSON file. If your notes were made signed out, use Bring over guest data first, so they are in your profile.Don’t copy the notes/ folder onto the volume: on the site its guest files can be read by every visitor. Bring data over only with Export profile and Import profile file.
ESV and NLT on the site. The owner sets the keys in Settings (⚙) on the site, just as locally. They are kept on the Railway volume (/data/config.json), never in the repo.
app/data/ is generated by build.py from the sources below (downloaded into raw/ on first run):
python3 build.py
Inputs kept in src/: cross_references.txt (OpenBible.info), topics/ (OpenBible.info topical index, from the Obsidian vault export), book_intros.json (hand-written book introductions — edit freely).
Bible art — Gustave Doré (1832–1883), engravings for La Grande Bible de Tours (1866), public domain, via Wikimedia Commons. The 222 plates on canonical passages (the Apocrypha plates are left out) come from Commons scans of Doré's English Bible and, where those are poor or missing, of other printings of the same engravings, mostly the 1874 Warsaw Bible scanned by the National Library of Poland. Each plate links to its Commons file page in app/data/art.json. src/art/dore.json is the curated list: the passage each plate illustrates, a short scene description, the chosen Commons file and any crop. python3 tools/fetch_art.py downloads the images into app/data/art/ (1800px and 560px JPEGs, about 270 MB) and rewrites art.json. It only fetches missing images (--force or --only dore-015 to redo some), and it uses Commons' resized thumbnails where they are big enough.
www.biblelantern.com and biblelantern.com only, never locally or on previews, and it sets no cookies./apiBS_OWNER_SETUP_TOKEN | a long random string | once a profile has claimed an owner address, anyone else who signs up with it gets an ordinary profile; this token, sent as setupToken in the sign-up body, lets the owner claim it again (for example after the owner profile was deleted or the data restored) |
BS_TURNSTILE_SITEKEY | from Cloudflare | Turnstile’s site key, which the app is given to show the check |
BS_TURNSTILE_SECRET | from Cloudflare | Turnstile’s secret key, sent only to Cloudflare to check a sign-up’s token and never logged. With both keys set, sign-up needs a passed check; if Cloudflare can’t be reached, sign-up waits (“The sign-up check is unavailable right now”) and the log gets an [error] line |
BS_SIGNUP | unset | closed pauses new sign-ups (the emergency switch): Home and the Create profile tab announce the pause (“New sign-ups are paused for now”, with Help us expand), and a refused sign-up gets “New sign-ups are paused for now while we expand. Reading stays open to everyone.”, signing in carries on, and an owner address can still sign up by the claim rules, through POST /api/auth/signup directly (the app’s form is hidden while paused), or by reopening sign-up for a moment. Remove it, or set open, to reopen |
BS_MAX_USERS | unset (5000) | the most profiles the site holds; at it, new sign-ups pause with the same message (locally --max-users, default 50, where the form says the computer has the maximum number of profiles) |
BS_PROFILE_QUOTA_MB | unset (25) | how much a profile’s notes, links, highlights and reading data may take on the volume (their daily backups come on top). A write that would go past it is refused with “Your profile has reached its storage limit. Delete some notes or highlights to make room.”; deleting always works. 0 turns it off; locally there is none |
BS_MIN_FREE_MB | unset (200) | when the volume has less free than this, or under 5%, new sign-ups pause with the same message while everything else carries on, and the log gets an [alert] line once an hour |
BS_DONATE_URL | unset (https://buymeacoffee.com/biblelantern) | where the server’s /donate and /support send people (a 302). Locally too; an http(s):// address without spaces |