A modern church app is infrastructure — sermons, events, and giving built on deterministic Scripture integrity and stable canonical data.
A step-by-step architecture that scales from one church to a multi-tenant platform—plus
exactly how to implement Scripture features using BibleBridge endpoints:
/api/resolve, /api/scripture, /api/search,
/api/votd, and /api/batch.
If you are implementing this in production, follow this sequence:
To answer “how do I build a church app?” you need more than a feature list. You need a layered architecture.
A stable flow prevents “canonical drift” and keeps offline usage reliable.
Canonical drift occurs when reference parsing rules change and stored strings no longer map deterministically to the same verse spans.
Mobile/Web App
+- Fetch church content (events, sermons, announcements)
+- Submit human Scripture references (e.g., "1 Cor 13", "Rom 8:1-4, 28")
|
v
Church Backend (your app)
+- Auth + roles (member/admin/pastor)
+- Tenant isolation (church_id)
+- Cache hot reads (Redis)
+- Store canonical coordinates for notes/bookmarks/sermons
|
v
BibleBridge API
+- /api/resolve (reference string -> canonical spans + OSIS + book_id)
+- /api/scripture (book_id/chapter/verse/range/endChapter/endVerse/version/compare)
+- /api/batch (bulk retrieval in one atomic request)
+- /api/search (full-text search by version; optional book_id scope)
+- /api/votd (daily verse for engagement + offline hydration)
|
v
PostgreSQL + Object Storage (S3) + Workers
Start with what churches expect by default. You can differentiate later—but you must cover the basics.
If you plan to serve multiple churches, isolate data from day one. Otherwise you’ll rewrite permissions, analytics, and content later.
model Church {
id Int @id @default(autoincrement())
name String
slug String @unique
createdAt DateTime @default(now())
users User[]
sermons Sermon[]
events Event[]
}
model User {
id Int @id @default(autoincrement())
churchId Int
email String @unique
role String // "member" | "admin" | "pastor"
createdAt DateTime @default(now())
church Church @relation(fields: [churchId], references: [id])
}
model Event {
id Int @id @default(autoincrement())
churchId Int
title String
startsAt DateTime
endsAt DateTime?
location String?
details String?
createdAt DateTime @default(now())
church Church @relation(fields: [churchId], references: [id])
@@index([churchId, startsAt])
}
Sermons are anchor content. Make them searchable, shareable, and linked to Scripture deterministically.
model Sermon {
id Int @id @default(autoincrement())
churchId Int
title String
speaker String?
preachedOn DateTime
mediaUrl String // S3 / CDN
notesHtml String?
createdAt DateTime @default(now())
church Church @relation(fields: [churchId], references: [id])
passages SermonPassage[]
@@index([churchId, preachedOn])
}
model SermonPassage {
id Int @id @default(autoincrement())
sermonId Int
// Canonical coordinates (BibleBridge resolver -> spans -> stored here)
book_id Int // 1-66
chapter Int
verseStart Int
verseEnd Int?
// Optional cross-chapter
endChapter Int?
endVerse Int?
sermon Sermon @relation(fields: [sermonId], references: [id])
@@index([book_id, chapter, verseStart, verseEnd])
}
Production systems cannot depend on loose string matching. Real-world input is messy and inconsistent: “1 Cor 13”, “I Cor. 13”, “First Corinthians 13”, or compound ranges like “Rom 8:1-4, 28; 12:1-2”. Before you store anything, normalize the reference into validated canonical coordinates.
curl --get https://holybible.dev/api/resolve \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "reference=rom 8:1-4, 28; 12:1-2"
// The resolver returns:
// - references[].book.book_id (canonical Book ID: 1-66)
// - references[].spans[] (validated start/end coordinates)
// - osis_id (OSIS-compatible identifier for interoperability)
//
// Persist canonical coordinates for each span:
// book_id, startChapter, startVerse, endChapter, endVerse
//
// Suggested storage shape:
// { book_id: 45, startChapter: 8, startVerse: 1, endChapter: 8, endVerse: 4 }
// { book_id: 45, startChapter: 8, startVerse: 28, endChapter: 8, endVerse: 28 }
//
// Optional: also store osis_id for share URLs and external interoperability.
Many Bible APIs provide verse retrieval by reference or internal content IDs. That solves text delivery. It does not solve canonical normalization.
BibleBridge separates Scripture reference integrity from text retrieval.
Inputs are first normalized into validated, canon-aware coordinates, then retrieved deterministically.
This eliminates ambiguity, enforces structural correctness, and protects long-term schema stability.
Once canonical coordinates are stored (book_id, chapter, verse or range),
retrieve Scripture text structurally using /api/scripture.
This enforces canon-aware bounds, preserves cross-translation alignment,
and eliminates scraping fragility.
Never store Scripture as raw strings. Store canonical coordinates, then resolve text deterministically at read time.
curl --get https://holybible.dev/api/scripture \
-H "Authorization: Bearer YOUR_API_KEY" \
--data book_id=19 \
--data chapter=23 \
--data verse=1 \
--data version=KJV
{
"status": "success",
"type": "single_verse",
"version": "KJV",
"book": {
"id": 19,
"name": "Psalm"
},
"chapter": 23,
"range": null,
"results_count": 1,
"data": {
"verse": 1,
"text": "A Psalm of David. The LORD is my shepherd; I shall not want."
}
}
Mapping tip: use book.id, chapter, and data.verse
to align directly with your stored canonical coordinates.
curl --get https://holybible.dev/api/scripture \
-H "Authorization: Bearer YOUR_API_KEY" \
--data book_id=19 \
--data chapter=23 \
--data range=1-6 \
--data version=KJV
curl --get https://holybible.dev/api/scripture \
-H "Authorization: Bearer YOUR_API_KEY" \
--data book_id=19 \
--data chapter=23 \
--data version=KJV
curl --get https://holybible.dev/api/scripture \
-H "Authorization: Bearer YOUR_API_KEY" \
--data book_id=1 \
--data chapter=1 \
--data verse=1 \
--data endChapter=2 \
--data endVerse=3 \
--data version=KJV
BibleBridge supports server-side verse alignment across two translations using the compare parameter.
This eliminates client-side joins and translation-specific indexing assumptions.
curl --get https://holybible.dev/api/scripture \
-H "Authorization: Bearer YOUR_API_KEY" \
--data book_id=19 \
--data chapter=23 \
--data range=1-3 \
--data version=KJV \
--data compare=ASV
Sermons often reference multiple passages. Reading plans pull multiple chapters.
Use POST /api/batch to retrieve many references in one atomic request with deterministic ordering.
curl -X POST https://holybible.dev/api/batch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"version": "KJV",
"references": [
{ "book_id": 19, "chapter": 23, "range": "1-6" },
{ "book_id": 45, "chapter": 8, "verse": 28 }
]
}'
Scripture reads are cacheable. Your app should be fast even on weak Sunday networks. Cache resolver outputs and retrieval outputs separately.
Many users will enter the same references repeatedly (e.g., “Psalm 23”). Cache resolved coordinates for a long TTL.
// Pseudocode
const getResolved = async (input) => {
const key = `resolve:${hash(input.toLowerCase())}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const url = "https://holybible.dev/api/resolve";
const res = await fetch(`${url}?reference=${encodeURIComponent(input)}`, {
headers: { "Authorization": `Bearer ${process.env.BIBLEBRIDGE_KEY}` }
});
const json = await res.json();
await redis.setex(key, 86400, JSON.stringify(json)); // 24h
return json;
};
Cache the result of /api/scripture (or /api/batch) for common reads: sermon passages, reading plan, Psalm 23, John 3:16.
// Key by version + canonical coordinates
// Examples:
// scripture:KJV:19:23:range:1-6
// scripture:KJV:1:1:1-2:3 (cross-chapter)
// scripture:KJV:19:23 (full chapter)
const cacheKeyFor = (params) => {
// params: { version, book_id, chapter, verse, range, endChapter, endVerse, compare }
// Keep compare separate since response schema differs.
};
Church apps have a unique traffic profile: minimal usage during the week, followed by concentrated spikes during live services. Scripture content is immutable per translation, making it ideal for edge caching.
/api/scripture responses
at the edge. On a busy Sunday, requests for the sermon passage should
be served from the CDN without hitting your application server.
// Example backend header strategy
Cache-Control: public, max-age=3600, s-maxage=86400
// Pre-warm likely passages before service:
// - Sermon text
// - Reading plan passages
// - Verse of the Day
This layered approach (CDN → Redis → BibleBridge) ensures deterministic performance under burst load.
Offline mode should be intentional. Don’t aim for “the whole Bible offline” at first. Aim for “the service experience works offline.”
/api/votd and store locally/api/scripture or /api/batchcurl https://holybible.dev/api/votd \
-H "Authorization: Bearer YOUR_API_KEY"
Track offline note/bookmark changes in a queue table and sync them when connectivity returns.
model SyncQueue {
id Int @id @default(autoincrement())
userId Int
type String // "note.create" | "bookmark.add" | "prayer.create" | ...
payload Json
createdAt DateTime @default(now())
sentAt DateTime?
status String @default("pending")
@@index([userId, status, createdAt])
}
Search is where most church apps feel slow. Use BibleBridge full-text search and scope it to reduce noise.
curl --get https://holybible.dev/api/search \
-H "Authorization: Bearer YOUR_API_KEY" \
--data search=love \
--data version=KJV
book_idMany apps combine hosted search with local search for cached sermon passages and offline experiences. Use a small local index (SQLite FTS) for cached passages only—not the entire corpus.
Production apps must respect provenance and licensing. BibleBridge serves verified public-domain Scripture editions and stable canonical schemas to avoid takedowns and silent data drift.
| Requirement | BibleBridge | Scraping |
|---|---|---|
| Attribution | No attribution required (public-domain sources) | Often mandatory / restricted |
| Commercial use | Permitted | Often prohibited |
| Schema stability | Stable canonical IDs | Brittle / dynamic |
| Maintenance | API contract + OpenAPI | Breaks when HTML changes |
This is the clean production workflow you want:
/api/resolve to normalize and validate/api/scripture (or /api/batch)compare for study mode// 1) Resolve
curl --get https://holybible.dev/api/resolve \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "reference=rom 8:1-4, 28"
// 2) Store returned book_id and spans as canonical coordinates
// 3) Retrieve (example for Romans 8:1-4)
curl --get https://holybible.dev/api/scripture \
-H "Authorization: Bearer YOUR_API_KEY" \
--data book_id=45 \
--data chapter=8 \
--data range=1-4 \
--data version=KJV
/api/resolve to sermon admin input.book_id, startChapter, startVerse,
endChapter, and endVerse in your database.
/api/scripture.Cache-Control headers so sermon passages
are served from the edge during peak traffic.
This is not a verse API. It is canonical data infrastructure for production systems.
BibleBridge provides deterministic Scripture reference integrity, structured retrieval, batch operations, full-text search, and cross-translation alignment — ensuring your church app remains stable, consistent, and canonically correct as it scales.
Next step: implement /api/resolve, store canonical coordinates, then render with /api/scripture.