A GraphQL API for querying Bible verses and passages across multiple translations. Supports localized book names so you can query in English ("John 3:16"), Spanish ("Juan 3:16"), and dozens of other languages.
- 40+ Bible translations in 30+ languages — public domain / freely licensed from open-bibles, plus extras from Bible List. Query
translationsorlanguagesfor the current figures. - Flexible passage lookup — single verses, ranges, multi-ranges (e.g.,
"Matthew 25:31-33,46") - Localized book names — query using book names in the translation's language (e.g.,
"Mateo 28:18-20"for Spanish) - Full-text search across verses
- Semantic search — find verses by meaning using AI embeddings (pgvector + RubyLLM), currently available for spa-rv1909
- Bible concordance — exhaustive word lookup with canonical ordering, per-book distribution, and keyword-in-context snippets
- Verse of the Day — curated daily verse for any translation and date
- Language discovery — list all available languages with translation counts
- Translation hierarchy — browse books, chapters, and verse counts per translation
- Bible index — full structural overview of any translation
- API Key authentication with environment-aware prefixes (
bql_live_/bql_test_) - Rate limiting — 100 req/min per IP, 1,000 req/day per API key
- Interactive Playground at
/playgroundfor exploring the API - Admin panel at
/adminfor managing API keys and requests
Full API documentation, in English and Spanish: docs.bibleql.org
Guides, a quickstart, authentication, and a GraphQL reference generated from the schema itself.
The site lives in docs/website and is deployed by
.github/workflows/docs.yml — see
CONTRIBUTING.md to run it locally.
The interactive GraphQL playground is available at /playground and lets you explore the API with example queries and a headers panel for authentication.
- Ruby 4.0 / Rails 8.1
- PostgreSQL
- graphql-ruby
- bible_parser — parses USFX/OSIS/Zefania XML Bible files
- bible_ref — parses Bible reference strings
- open-bibles — public domain Bible translations (git submodule)
- Bible List — searchable collection of 1,550+ XML Bibles, source of the additional translations in
db/biblelist/ - Holy-Bible-XML-Format — 1,000+ XML Bibles across 200+ languages, downloaded individually as needed (not a submodule)
- Docker + Kamal for deployment
| Language | Package | Source |
|---|---|---|
| Ruby | bibleql-ruby (RubyGems) |
lporras/bibleql-ruby |
| Node.js / TypeScript | bibleql-js (npm) |
lporras/bibleql-js |
- Examples: bibleql-example — working examples using both SDKs
- Setup guide: Configuring the SDKs
- Ruby 4.0+
- PostgreSQL
- Git (for submodules)
# Clone the repository (with submodules)
git clone --recurse-submodules https://github.com/lporras/bibleql.git
cd bibleql
# If you already cloned without submodules
git submodule update --init
# Install dependencies
bundle install
# Local environment variables (all optional — see below)
cp .env.example .env
# Create and migrate the database
bin/rails db:create db:migrate
# Import Bible translations (all ~45 open-bibles translations)
bundle exec rake bible:import
# Or import a single translation
bundle exec rake "bible:import_one[eng-web]"
# Build the concordance index (required for concordance queries)
bundle exec rake concordance:indexLocal settings live in .env, which is gitignored and loaded by dotenv-rails. .env.example lists every variable with a comment explaining it. Nothing is required to run the API locally: each feature below is simply off until its variables are set.
| Variables | Needed for |
|---|---|
DATABASE_URL |
A database other than the local bibleql_development |
OPENAI_API_KEY |
The semanticSearch query |
OFFLINE_R2_ACCOUNT_ID, OFFLINE_R2_ACCESS_KEY_ID, OFFLINE_R2_SECRET_ACCESS_KEY, OFFLINE_R2_BUCKET, OFFLINE_PUBLIC_BASE_URL |
Exporting offline translation packages (see below) |
DOCS_URL |
Pointing documentation links at a local bin/docs server |
Redistributable translations can be exported as gzipped SQLite files that clients download once and use offline (format and client contract). The files are uploaded to a Cloudflare R2 bucket. To set it up for development:
- In the Cloudflare dashboard, create an R2 bucket.
- In R2 → Manage API tokens, create an Object Read & Write token scoped to that bucket. Copy the Access Key ID and Secret Access Key (the secret is shown only once). Your Account ID is in Account Details on the R2 overview page.
- In the bucket's Settings, enable the Public Development URL (
https://pub-<hash>.r2.dev). - Put the five
OFFLINE_*values in.env, then check them:
bundle exec rake offline:check_storage # uploads a probe object and fetches it back publicly
# Only translations whose license allows redistribution may be exported (default: none)
DRY_RUN=1 bundle exec rake offline:flag_public_domain
bundle exec rake "offline:flag[spa-rv1909,true]"
# Exports run as background jobs, so start the worker (bin/dev starts it too)
bin/jobs
bundle exec rake "offline:export_one[spa-rv1909]"
bundle exec rake offline:listExports can also be started from the admin panel under Offline Packages, and every import re-exports downloadable translations automatically.
Beyond the open-bibles submodule, BibleQL can import XML Bibles from
Bible List — a searchable collection of 1,550+ XML Bibles.
Drop the downloaded files in db/biblelist/, describe them in
config/biblelist_translations.yml, and import them with their own command:
bundle exec rake biblelist:list # show configured files and import status
bundle exec rake biblelist:import # import all of them
bundle exec rake "biblelist:import_one[eng-niv]" # import a single oneThese files use a different XML shape than the open-bibles formats (<bible><testament><book number="1">), handled by the bible_parser format plugin in lib/biblelist_format/. They
identify books by ordinal number only, so their localized book names are copied from an
already-imported translation of the same language (book_names_from in the YAML) — run
rake bible:import first.
Note: most translations available through Bible List are copyrighted. Confirm you hold the necessary rights before exposing any of them through a public API.
A third source, Holy-Bible-XML-Format, is a
1,000+ file, 200+ language collection. It's not a git submodule: Render (and most git-based
deploy pipelines) automatically fetch every submodule registered in .gitmodules on every deploy,
which isn't workable for a source this large. Instead, download only the specific translations you
want, one file at a time, into db/holy-bible-xml/ — that directory is gitignored, so nothing you
put there is ever committed to this repo or fetched by CI/Render.
Browse the repo for the filename you want
(they follow [LanguageName][Variant]Bible.xml, e.g. ArabicSVDBible.xml,
SpanishNTVBible.xml), download it, then import it by the identifier derived from its filename
(see docs/holy-bible-xml-identifiers.md for exactly how
that derivation works — e.g. ArabicSVDBible.xml → ara-svd):
mkdir -p db/holy-bible-xml
curl -fsSL -o db/holy-bible-xml/ArabicSVDBible.xml \
https://raw.githubusercontent.com/lporras/Holy-Bible-XML-Format/master/ArabicSVDBible.xml
bundle exec rake "holy_bible_xml:import_one[ara-svd]"These files use the same XML shape as db/biblelist/'s, so they're parsed by the same
BiblelistFormat::Parser. Unlike db/biblelist/, there's no hand-curated metadata file for
1,000+ translations — HolyBibleXmlFilenameParser derives each translation's identifier,
language code, and abbreviation straight from its filename, falling back to a slugified language
name for anything not in config/language_codes.yml. import_one skips (rather than re-imports)
any identifier that's already in the database, so re-running it never creates duplicates. Book
names are copied from whichever already-imported translation of the same language has the most of
them, falling back to the canonical book name — run rake bible:import first.
bundle exec rake holy_bible_xml:import will import every *.xml file already sitting in
db/holy-bible-xml/ (skipping any already-imported identifiers) if you've downloaded more than
one — there's just no bulk-download step for the full 1,000+ file source, by design.
These tasks are meant to be run locally (or from any machine with the files downloaded) against
whichever database you point DATABASE_URL/RAILS_ENV at, including production:
RAILS_ENV=production DATABASE_URL=<production-db-url> bundle exec rake "holy_bible_xml:import_one[ara-svd]"HolyBibleXmlImporter builds the concordance index automatically as the last step of every
import (same as every other importer), so freshly imported translations are immediately
queryable via concordance/concordanceIndex. If you ever need to rebuild the index for a
specific translation by hand — e.g. after editing config/language_codes.yml and re-importing,
or to pick up a new text_search_config — target it directly by identifier instead of
reindexing everything:
bundle exec rake "concordance:index[spa-ntv]"
bundle exec rake "concordance:index[spa-tla]"
bundle exec rake concordance:status # confirm indexed_at / stemming per translationNote: confirm you hold the necessary rights before exposing any imported translation through a public API.
bin/dev # Rails server, Tailwind watcher, Solid Queue worker and docs site
# or just the API:
bin/rails serverBackground jobs (offline package exports, emails) run on Solid Queue. With plain bin/rails server, start bin/jobs in another terminal to process them.
- GraphQL endpoint:
POST http://localhost:3000/graphql - Playground:
http://localhost:3000/playground - GraphiQL IDE:
http://localhost:3000/graphiql(development only) - Admin panel:
http://localhost:3000/admin(development only)
All POST /graphql requests require an API key via the Authorization header:
Authorization: Bearer bql_live_xxxxxxxxxxxxxxxx
- Token prefixes:
bql_live_(production),bql_test_(development/test) - Get an API key: Visit
/api-keys/request/newto submit a request (admin approval required) - Manage keys via rake:
# Create a key
bundle exec rake "api_keys:create[name,email,environment]"
# List all keys
bundle exec rake api_keys:list
# Revoke a key
bundle exec rake "api_keys:revoke[prefix]"{
passage(translation: "eng-web", reference: "John 3:16") {
reference
text
translationName
verses {
bookName
chapter
verse
text
}
}
}Response:
{
"data": {
"passage": {
"reference": "John 3:16",
"text": "For God so loved the world, that he gave his one and only Son, that whoever believes in him should not perish, but have eternal life.",
"translationName": "World English Bible",
"verses": [
{
"bookName": "John",
"chapter": 3,
"verse": 16,
"text": "For God so loved the world, that he gave his one and only Son, that whoever believes in him should not perish, but have eternal life."
}
]
}
}
}| Query | Description |
|---|---|
translations |
List all available translations |
translation(identifier) |
Get a single translation with nested books and chapters |
books |
List all 66 canonical books |
languages |
List all languages with translation counts |
passage(translation, reference) |
Look up a passage (e.g., "John 3:16", "Mateo 28:18-20") |
chapter(translation, book, chapter) |
Get all verses in a chapter |
verse(translation, book, chapter, verse) |
Get a single verse |
search(translation, query, limit) |
Case-insensitive substring search across verses (no stemming — use concordance for that) |
semanticSearch(query, translation, limit) |
Search verses by semantic meaning using AI embeddings |
randomVerse(translation, testament, books) |
Get a random verse with optional filters |
verseOfTheDay(translation, date) |
Get the curated verse of the day |
bibleIndex(translation) |
Get the structural hierarchy (books, chapters, verse counts) |
concordance(translation, word, book, testament, first, after) |
Exhaustive, canonically-ordered concordance for a word, with per-book counts and KWIC context |
concordanceIndex(translation, prefix, minOccurrences, first) |
Alphabetical word index with occurrence frequencies |
Each query is documented with runnable GraphQL, cURL, Ruby and Node.js examples at docs.bibleql.org. See also docs/example_queries.md for raw request/response pairs for every query.
The passage query supports these reference formats:
| Format | Example |
|---|---|
| Single verse | "John 3:16" |
| Verse range | "John 3:16-18" |
| Multiple ranges | "Matthew 25:31-33,46" |
| Full chapter | "Genesis 1" |
| Cross-chapter | "Romans 12:1,3-4 & 13:2-4" |
| Localized names | "Mateo 28:18-20", "Lucas 3:1-10" |
Unlike search, which returns the top-N most relevant verses, concordance returns every
occurrence of a word in canonical order, along with aggregate counts.
Stemming availability varies by language. PostgreSQL ships dictionaries for about 30 languages;
translations in other languages fall back to exact form matching. Check translation.hasStemming
to know which behavior applies.
Run rake concordance:index after importing translations, or concordance queries will return
an error prompting you to build the index.
The API is protected by rate limiting via Rack::Attack:
| Scope | Limit |
|---|---|
| GraphQL requests per IP | 100/minute |
| GraphQL requests per API key | 1,000/day |
| API key request form per IP | 5/hour |
Exceeded limits return a 429 Too Many Requests response with a Retry-After header.
bundle exec rspecbin/rubocopBibleQL is deployed using Docker and Kamal. See config/deploy.yml and Dockerfile for details.
Production needs SOLID_QUEUE_IN_PUMA=true so the web process also runs the background-job worker, and, for offline packages, the five OFFLINE_* variables pointing at the production bucket (with a custom domain such as https://downloads.bibleql.org as OFFLINE_PUBLIC_BASE_URL).
The CI pipeline (GitHub Actions) runs security scans (Brakeman, Bundler Audit), linting (RuboCop), and tests (RSpec) on every PR.
See CONTRIBUTING.md for guidelines on how to contribute to this project.
This project is open source. Bible translations included via open-bibles are Public Domain or Creative Commons licensed.
