Solutions architecture · In weekly use · v1.17.1 · Open source (AGPL‑3.0)
OpenOrder
Fill in one form and get a print-ready Word bulletin and a complete worship slide deck, each in the congregation’s own template and palette, with scripture that works offline. It has built the projected deck for 29 Sundays in a row.
Status
In weekly use since 29 Mar 2026, now for two congregations. No published binaries yet.
Version
1.17.1, build 28 (2 Oct 2026); macOS and Windows builds from one commit.
Licence
AGPL-3.0. The bundled Berean Standard Bible is public domain. Hymnal lyrics are licensed, supplied by the user and never in the repo.
Copying hymn verses onto slides one at a time and rebuilding the bulletin every week.
→
Now
One 16-field form (a real service fills a median of 14) and two documents out, in the congregation’s own template.
At a glance
consecutive Sundays it built the projected deck, 29 Mar to 11 Oct 2026, with no gaps
29
slides per projected deck, median 59, across 31 decks for two congregations
47–85
hymnal entries extracted from a legacy format, kept on the user’s machine
1,158
Bible verses bundled for offline use; the macOS build’s file is SHA‑256-identical to the publisher’s
31,102
How these were measured
29 and 47–85: on 12 Oct 2026 a read-only script opened every deck in the archive of decks actually projected. A deck counts as generated when its slides use only the generator’s typeface, Georgia; the hand-built decks up to 22 March use Arial and Times New Roman. 31 decks qualify, on 29 Sundays, each seven days after the last. Slide counts are the slide parts inside each .pptx (median 59, mean 62.2).
1,158: entries in the extracted hymnal index on the machine that runs it, in five source sets. git ls-files hymnal-json returns nothing, because the folder is gitignored.
31,102: verse lines counted with grep -c in bible-text/bsb.txt (66 books). The copy inside the installed macOS app has the same SHA‑256 as bereanbible.com/bsb.txt fetched that day. Git stores the file with normalized line endings, so a fresh clone holds the same text (the 31,102 verses plus a three-line header) in different bytes.
16 fields, median 14 filled: the field count is from the service model in backend/app/models.py; the median is over 23 saved services on 20 Sundays, counting the date and the defaulted translation and prayer.
Fig. 1 The entry form: hymn numbers resolve to titles, each tagged with its hymnal source, and the sermon block below takes a reference and a translation. Captured in July 2026, before the multi-congregation releases. Cropped to the hymn pickers; the image links to the full capture.
The short version
Extracted 1,158 hymnal entries (hymns, psalms, creeds and liturgies) from a legacy binary format, and kept them out of the repo. PowerPoint converts the files; the extractor reads the layout, not the wording.
Made scripture work offline, and say so when it substitutes. The default translation ships in the app, and a failed network fetch falls back to it with the reason in plain words.
Built autosave with revert and optimistic concurrency, so a finished service is never silently lost. 20 revisions per service, a 412 that waits for Load theirs or Keep mine, and generated files that take a new name instead of replacing an old one.
Case 1 of 5
Copyrighted lyrics in a format the toolchain can’t read
The lyrics arrive as legacy .ppt files that python-pptx can’t open and no public repo may hold, so PowerPoint converts them and a layout-based extractor reads them.
The hymn and liturgy slides come from purchased presentation editions of two licensed hymnal collections, sold on a USB drive or as a download, in a binary format python-pptx can’t open.
A 92-line PowerShell script drives the real PowerPoint application over COM and saves each file as .pptx, skipping cloud-sync conflict copies. A 347-line extractor then finds the lyrics by where shapes sit, not what they say, because the wording varies from file to file and the layout doesn’t:
shapes parked off the slide are dropped;
title and hymn-number boxes are skipped by position;
the lyric is the tallest shape below the 1.5-inch line;
copyright lines become metadata.
The 1,158 entries live on the user’s machine and are gitignored in the repo. The Windows build carries no lyrics and finds the hymnal through a folder setting. With none configured, the app says so, instead of returning searches that look like “no such hymn”.
How we know
Read 12 Oct 2026 from the public repo: scripts/convert_ppt_to_pptx.ps1 (92 lines) and scripts/extract_lyrics.py (347 lines). The 1,158 is the length of the local hymnal index, in five source sets; build 28’s Windows archive (976 files) contains no hymnal data; the hymnal-missing notice arrived in 1.15.0.
Case 2 of 5
Scripture that works with the cable unplugged, and says so when it substitutes
A missing reading used to fail silently; now the default translation ships inside the app, and every substitution is announced in plain words.
Every translation was once fetched over the network. With no connection, a deck came out looking complete with the reading missing, and nothing warned anyone.
The fix has two halves. The public-domain Berean Standard Bible now ships inside the app, in the publisher’s own plain-text format, so the default needs no network. When a networked translation fails, the bundled text stands in and the app says why (no connection, a rejected key, or a passage that translation lacks), in the preview and after generation.
One interface fronts three sources: the bundled text, a free API for KJV, ASV and WEB, and API.Bible with the congregation’s own key, validated before it’s stored and never returned. Traps found on the way:
“John 14:15-17, 25-27” splits into contiguous runs, so the gap isn’t swallowed, and a semicolon starts a second reading;
slides are titled from the passage the API actually returned, not the one requested, because paraphrases come back as whole paragraphs;
two book names differ from the parsing library’s, and one of them is Psalms;
one cache policy (30 days, under 500 verses) meets the strictest provider’s terms.
How we know
Read 12 Oct 2026 from the public repo: backend/app/scripture.py (the three fallback reasons, contiguous runs, the cache limits), bible_local.py (two book-name aliases), bible_api.py (a key is checked by listing Bibles before it is saved), and main.py (the key endpoint reports status only, never the key). Released in 1.10.0, 1.11.0 and 1.16.0.
Case 3 of 5
Autosave without ever losing a Sunday
Once the form saved itself, closing without saving stopped being an undo, so four layers replaced it, each closing a failure that actually happened.
Revisions. A snapshot at every point where work could be lost (opening a service, switching congregation, generating, starting a new one), 20 deep, behind a Revert button.
Refusals in words. A failed save returns a 409 with a reason and blocks the move to another service. An empty form can’t overwrite a saved one unless the clear was deliberate.
The date comes first. “New service” once cleared the form but kept the date, so autosave blanked a finished service. Now a new service asks for its date, a taken date is refused, and the clearing function is gone.
Optimistic concurrency, in 1.17.1. Two windows or two computers can share one data folder, so reads carry a modification token and each autosave sends it back. A mismatch gets a 412, and nothing is written until the person chooses.
Generation never overwrites either: a taken name becomes “name (2)”. Each file’s receipt is read back, so a save that didn’t happen raises an error instead of a success message.
Fig. 2 Optimistic concurrency, as shipped in 1.17.1. The notice says where each copy is.How we know
Read 12 Oct 2026 from the public repo: backend/app/main.py (20 snapshots kept per service, the token header, the 412 check, the empty-form refusal and the worded 409s), frontend/src/App.tsx (the conflict notice and its two buttons), backend/app/paths.py (never overwrite) and the write receipt shared by the bulletin and slide generators. Released across 1.7.4, 1.8.0, 1.9.0, 1.14.0, 1.17.0 and 1.17.1 (the CHANGELOG documents 1.8.0, 1.9.0 and 1.17.0).
Case 4 of 5
Two congregations, one codebase: generalize at the second case
Multi-congregation support waited for a real second congregation, and that second case exposed defects one case never could.
I built it in four feature releases, 1.5.0 to 1.8.0, over two days. Each congregation is a folder in the user’s data directory, never in the package, holding its Word template, palette, seven art slots, communion template and calendar. A built-in default makes a fresh install work, and a broken profile or a bad colour falls back without costing a Sunday.
Three defects surfaced that a single congregation had never triggered:
the bulletin’s calendar block was drawn in a hard-coded accent colour;
python-pptx and python-docx each define an RGBColor that fails isinstance against the other’s, so theming silently fell back; colours now cross between them as hex;
backdrops sat on top of the text, hidden only by translucent art; z-order is now explicit.
Section titles are composited in Pillow in 11 title styles, several of them true frosted glass, because a PowerPoint shape can’t blur what’s behind it. In the same two days, service titles began coming from the church year, computed in code from the date: Easter by the Meeus/Jones/Butcher algorithm, Advent counted back from Christmas, and a three-tier precedence for days with more than one name.
1.5.0 Congregations become folders: template, palette, art.
1.6.0 Backdrops move behind the words.
1.7.0 The second congregation gets its own calendar and accent colour.
1.8.0 One service per congregation per Sunday.
The second congregation’s first projected deck.
How we know
Read 12 Oct 2026 from the public repo: backend/app/churches.py (the default profile, seven image slots, per-key colour fallback), backend/app/slide_art.py (11 title styles), backend/app/slides.py (explicit z-order), frontend/src/lib/church-year.ts (Easter by the Meeus/Jones/Butcher algorithm, Advent counted from Christmas, three tiers of precedence) and the CHANGELOG for 1.5.0 to 1.9.0. The October date comes from the deck archive, where the second congregation’s decks carry their own palette and art.
Case 5 of 5
One commit, two platforms, one build number, one hash
One script builds macOS and Windows from the same commit under one build number, and the 976-file Windows artifact travels as one zip with one SHA‑256.
The real work is in Python: document generation, the hymnal, scripture. So the app is a PyInstaller bundle that starts FastAPI on a background thread and opens a native pywebview window on it: one process, no sidecar.
One script runs the release. It gates on a build-mode typecheck (the solution-style tsconfig means the plain command checks nothing) and a spelling check over every tracked file, then refuses a dirty tree. The macOS leg claims the build number before the frontend compiles, so the number in the app’s footer is never off by one; a separate Windows machine builds the same commit under the same number. Its 976 files travel as one zip, so one SHA‑256, checked on every copy, proves the whole artifact arrived.
Each fix is a postmortem turned into a step: the PyInstaller work directory is cleared (cached builds kept shipping a stale icon), the macOS bundle is re-signed last and verified, and pushes name their remote and branch.
Fig. 3 One release, in script order. Steps outlined in amber stop it when they fail.How we know
Read 12 Oct 2026 from the public repo: scripts/build_all.sh (gate order, the dirty-tree refusal, the named pushes), build.sh, scripts/build_win.ps1 and scripts/fanout_win.sh. The 976 is a count of entries in build 28’s Windows zip. The spelling gate was run that day with node scripts/check_britishisms.mjs, which only reads files: 0 hits across 50 tracked files.