One person picks the hymn; everyone else's phone follows. Built for singing together in a room, on bad wifi, from phones nobody wants to fiddle with.
npm install && npm --prefix web install
npm run build:library # content/hymns/*.hymn → web/public/library.json
npm run dev # API on :8787, web on :5173
Then open the web URL, tap Ξεκίνα σύναξη, enter the host PIN, and read the four-digit session code aloud. Everyone else types it on the same page.
Anyone with the link can view — that is the point. Starting a session needs a PIN, or the active-sessions list fills up with half-abandoned rooms and nobody knows which one to join.
HYMN_HOST_PIN=1234 npm start
Without it the server generates one per boot and prints it on startup, so the app is never accidentally left open. It is checked on the server, so it cannot be clicked past. A phone remembers a PIN the server accepted, so a host types it once, not every week.
A second person can co-host: the host taps Πρόσθεσε co-host for a six-digit code, good for ten minutes and one use, typed into the same box on the home screen. Both share one queue.
For production: npm run build && npm start, or docker build -t hymns . && docker run -p 8787:8787 -v hymn-data:/data hymns.
content/hymns/**/*.hymn → build-library.ts → web/public/library.json
│ (+ .br / .gz siblings;
│ 4.9 MB → 0.70 MB on the wire)
▼
precached by the service worker
│
Hono + ws + node:sqlite ──── hymn ids only ─────────►│ React PWA
(rooms survive a restart) │
The bundle is pre-compressed at build time and content-negotiated, so a phone
downloads 0.70 MB (brotli) for all 2703 hymns, once. library.json is served
no-cache and revalidates to a 304, because it lives under a name that never
changes and a phone quietly holding last month's hymnbook is worse than a slow load.
The socket only ever carries a hymn id. Every phone already holds the complete hymnbook, so changing hymn is a few dozen bytes, it lands instantly on church wifi, and a phone that drops off keeps the words it is showing. That one decision is why the app is usable in the building it was built for.
Everything about how a hymn looks — chords on or off, transposition, font size, light or dark — is per-device and never leaves it. The guitarist reads chords in D while the person beside them reads clean words in A.
Picking a hymn never moves the room on its own. The host opens it, reads enough to be sure it is the one they meant — titles in this library repeat and differ by a word — and then taps Στείλε σε όλους. Only the queue's Επόμενο pushes straight through, because those hymns were already vetted when they were queued.
| Path | What |
|---|---|
content/hymns/ | 2703 .hymn files — the source of truth, in git |
content/media/ | παρτιτούρα and page scans (gitignored; see below) |
packages/hymn-format/ | parser, validator, chord transposer, Greek/Greeklish search |
scripts/ | importers, build-library.ts, validate-hymns.ts |
server/ | Hono API, rooms, WebSocket |
web/ | React PWA |
docs/ | format spec, collection brief, import notes, review queue |
npm test # 104 unit tests over the format package and search
npm run validate:hymns # every .hymn file, exits non-zero on any error
npm run build:library # fails on a duplicate id or number
docs/IMPORT-NOTES.md is required reading before touching an importer, and
docs/REVIEW-QUEUE.md lists everything a human still needs to judge. No
hymn in the collection has been proofread yet — they all carry
verified: false, and only a person should change that.
docs/REVIEW-QUEUE.md lists 96 more chord sets that were found
but could not be applied automatically.content/media/ is gitignored, so scans resolve only on the machine
that ran the import. Decide git-lfs vs. an external mirror before fetching
the ~500 MB of ymnoi.gr scores.Content type
Image
Digest
sha256:49610e336…
Size
65.4 MB
Last updated
6 days ago
docker pull giorgospap777/hymn-app