Building a .lamp file
The mechanics, end to end. A .lamp is a SQLite database with a specific set of tables, compressed with raw DEFLATE — no proprietary tooling involved.
What a .lamp file actually is
Three things, in this order:
- A SQLite database containing the tables for one module type, including a
module_formatrow that names the module and its type. - Compressed with raw DEFLATE — zlib level 9,
wbits=-15, no zlib or gzip header. - Saved as
<module id>.lamp. The app registers the extension as a document type, so the file opens straight into Lamp Bible from Files, Mail or a share sheet.
The wbits=-15 detail is the one people get wrong. A standard zlib.compress() or gzip produces a header the app’s decompressor will not accept. It must be a bare DEFLATE stream.
The file name matters too. On iPhone and iPad the app takes the module’s identifier from the file name, then refuses the file if any of its rows belong to a different module. A module whose ID is MYBIBLEs must travel as MYBIBLEs.lamp.
The reference implementation
The sample modules published on this site are produced by a single self-contained Python script with no third-party dependencies — SQLite and DEFLATE are both in the standard library. It is written to be read, and it is the shortest complete answer to “how do I make one of these”. Every sample on the downloads page comes out of it, and each one is checked against the app’s own compiler: given the same JSON, the app writes the same tables and the same rows.
1. Create the tables
Each module type has its own set of tables, and every type has the module_format table. The definitions must match what the app reads; they are published here so you do not have to guess.
- translation.sql1.9 KB
- dictionary.sql1.0 KB
- commentary.sql1.5 KB
- book.sql1.7 KB
- plan.sql726 B
- quiz.sql1.3 KB
- notes.sql1.3 KB
- devotional.sql1.3 KB
- highlights.sql890 B
2. Write your rows
Insert your content using ordinary SQL, starting with the module_format row. Structured fields — annotations, footnotes, senses, readings — are stored as JSON text in their respective *_json columns.
import json, sqlite3
conn = sqlite3.connect('MYBIBLEs.db')
conn.executescript(open('translation.sql').read())
cur = conn.cursor()
# Which module this is, and what type. The app reads this row first.
cur.execute(
'INSERT INTO module_format (format_version, module_type, module_id) VALUES (?, ?, ?)',
('1.0', 'translation', 'MYBIBLEs'),
)
cur.execute(
'INSERT INTO translation_meta (id, name, abbreviation, language) VALUES (?, ?, ?, ?)',
('MYBIBLEs', 'My Bible', 'MYBIBLE', 'en'),
)
cur.execute(
"""INSERT INTO verses (ref, book, chapter, verse, text, annotations_json, paragraph)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(
1001001, 1, 1, 1,
'In the beginning God created the heaven and the earth.',
json.dumps([
{'type': 'strongs', 'start': 17, 'end': 20,
'text': 'God', 'data': {'strongs': 'H430'}}
]),
1,
),
)
# ...and a books row for each book, and headings, in the same way.
conn.commit()
conn.close()The module ID goes in three places: module_format, the type’s own metadata row, and the file name. They must all agree.
3. Compress it
import zlib
data = open('MYBIBLEs.db', 'rb').read()
compressor = zlib.compressobj(level=9, wbits=-15) # raw DEFLATE, no header
compressed = compressor.compress(data) + compressor.flush()
# Always verify the round trip before discarding the original.
assert zlib.decompressobj(wbits=-15).decompress(compressed) == data
open('MYBIBLEs.lamp', 'wb').write(compressed) # named after the module IDTypical compression is 95% or better, since SQLite pages are mostly empty space.
4. Verify before you ship it
Read it back the way the app will, and check four things:
data = zlib.decompressobj(wbits=-15).decompress(open('MYBIBLEs.lamp','rb').read())
assert data.startswith(b'SQLite format 3\x00') # it is a database
conn = sqlite3.connect(path_to_temp_file_containing(data))
assert conn.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
assert conn.execute(
'SELECT module_type, module_id FROM module_format'
).fetchall() == [('translation', 'MYBIBLEs')] # one row, matching the file name
assert conn.execute('SELECT count(*) FROM verses').fetchone()[0] > 05. Install it
Move the file onto the device — AirDrop, Files, iCloud Drive, or an email to yourself — and open it, or import it from the module manager in Settings. If a module with the same identifier is already installed you will be asked whether to overwrite.
Table definitions by type
These are the tables the app’s own compiler writes. The downloadable .sql files above are the complete definitions, indexes included; the versions here are condensed to show the shape.
Every module: module_format
One row naming the module and its type. The app reads it before anything else. Without it the app has to guess the type from the other tables, and a module that carries a module_metadata table is taken for a dictionary.
CREATE TABLE module_format (
format_version TEXT NOT NULL, -- "1.0"
module_type TEXT NOT NULL, -- translation | dictionary | commentary | book |
-- plan | quiz | notes | devotional | highlights
module_id TEXT NOT NULL -- the file name, without .lamp
);Translation
The largest schema, and one of three with a full-text index — the only one with an insert trigger to keep it current.
CREATE TABLE translation_meta (
id TEXT PRIMARY KEY, name TEXT NOT NULL, abbreviation TEXT NOT NULL,
description TEXT, language TEXT NOT NULL, language_name TEXT,
text_direction TEXT NOT NULL DEFAULT 'ltr', translation_philosophy TEXT,
year INTEGER, publisher TEXT, copyright TEXT, copyright_year INTEGER,
license TEXT, source_texts_json TEXT, features_json TEXT,
versification TEXT DEFAULT 'standard'
);
CREATE TABLE books (
id INTEGER PRIMARY KEY, -- book number, 1-66
book_id TEXT NOT NULL, name TEXT NOT NULL,
testament TEXT NOT NULL,
chapter_count INTEGER NOT NULL -- the book's highest chapter, even if some are absent
);
CREATE TABLE verses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ref INTEGER NOT NULL UNIQUE, -- BBCCCVVV
book INTEGER NOT NULL, chapter INTEGER NOT NULL, verse INTEGER NOT NULL,
text TEXT NOT NULL, -- plain text, used for display and search
annotations_json TEXT, -- Strong's, red-letter, and the rest
footnotes_json TEXT,
footnote_refs_json TEXT, -- where each footnote marker sits in text
paragraph INTEGER DEFAULT 0,
poetry_json TEXT
);
CREATE TABLE headings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book INTEGER NOT NULL, chapter INTEGER NOT NULL,
before_verse INTEGER NOT NULL, level INTEGER NOT NULL DEFAULT 1,
text TEXT NOT NULL
);
CREATE VIRTUAL TABLE verses_fts USING fts5(
text, content='verses', content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
CREATE TRIGGER verses_fts_insert AFTER INSERT ON verses BEGIN
INSERT INTO verses_fts(rowid, text) VALUES (new.id, new.text);
END;Dictionary
The one type with a module_metadata table. Each sense in senses_json carries its definition, gloss, partOfSpeech and references, with its number stored as a string.
CREATE TABLE module_metadata (
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
author TEXT, version TEXT, key_type TEXT, schema_version TEXT,
type TEXT, series_abbrev TEXT, series_full TEXT, editor TEXT,
publisher TEXT, year INTEGER, isbn TEXT, language TEXT
);
CREATE TABLE dictionary_entries (
id TEXT PRIMARY KEY, -- "<module id>:<key>"
module_id TEXT NOT NULL,
key TEXT NOT NULL, -- e.g. "H430", "G3056"
lemma TEXT NOT NULL,
transliteration TEXT, pronunciation TEXT,
senses_json TEXT, metadata_json TEXT,
search_text TEXT -- definitions and glosses, for searching
);Commentary
One book of a commentary series per file, and no metadata table: the series name and abbreviation and the volume title live on the single commentary_books row. Units form a self-referencing tree. unit_type is section, pericope or verse, with parent_id pointing at the enclosing unit; a chapter introduction is a section at level 0.
CREATE TABLE commentary_books (
id TEXT PRIMARY KEY, -- "<module id>:<book number>"
module_id TEXT NOT NULL, book_number INTEGER NOT NULL,
series_full TEXT, series_abbrev TEXT, title TEXT, author TEXT,
editor TEXT, publisher TEXT, year INTEGER,
abbreviations_json TEXT, front_matter_json TEXT, indices_json TEXT
);
CREATE TABLE commentary_units (
id TEXT PRIMARY KEY, module_id TEXT NOT NULL,
book INTEGER NOT NULL, chapter INTEGER,
sv INTEGER NOT NULL, ev INTEGER,
unit_type TEXT NOT NULL, -- section | pericope | verse
level INTEGER NOT NULL DEFAULT 1,
parent_id TEXT, title TEXT, suffix TEXT,
introduction_json TEXT, translation_json TEXT,
commentary_json TEXT, footnotes_json TEXT,
search_text TEXT NOT NULL DEFAULT '',
order_index INTEGER NOT NULL DEFAULT 0 -- reading order
);Book
A long-form publication: one row for the book, one per section. Sections nest through parent_id, and each holds its content blocks as JSON. Footnotes are defined once, for the whole book, and any section’s text can point at them.
CREATE TABLE book_modules (
id TEXT PRIMARY KEY, title TEXT NOT NULL, subtitle TEXT,
description TEXT, author TEXT, editor TEXT, publisher TEXT,
year INTEGER, edition TEXT, isbn TEXT, language TEXT NOT NULL,
text_direction TEXT NOT NULL DEFAULT 'ltr', copyright TEXT,
license TEXT, version TEXT, schema_version TEXT NOT NULL,
tags_json TEXT, cover_media_id TEXT,
is_editable INTEGER NOT NULL DEFAULT 0,
created INTEGER, last_modified INTEGER,
footnotes_json TEXT, -- shared by every section
media_json TEXT
);
CREATE TABLE book_sections (
id TEXT PRIMARY KEY, -- "<module id>:<section id>"
module_id TEXT NOT NULL REFERENCES book_modules(id) ON DELETE CASCADE,
section_id TEXT NOT NULL,
parent_id TEXT REFERENCES book_sections(id) ON DELETE CASCADE,
section_type TEXT NOT NULL, -- front-matter | part | chapter | section | ...
number TEXT, title TEXT NOT NULL, subtitle TEXT,
depth INTEGER NOT NULL, order_index INTEGER NOT NULL,
key_scriptures_json TEXT, content_json TEXT NOT NULL,
search_text TEXT NOT NULL DEFAULT '',
UNIQUE(module_id, section_id)
);
CREATE VIRTUAL TABLE book_sections_fts USING fts5(
title, search_text, content='book_sections', content_rowid='rowid'
);Notes and devotionals
The two editable text types, sharing a small module_meta table. Notes are keyed to a verse and cover one book per file; a chapter introduction is a note on verse 0. A devotional module is a single document, which may be dated or placed in a series.
CREATE TABLE module_meta (
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
author TEXT, version TEXT, is_editable INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE note_book_meta (
module_id TEXT PRIMARY KEY REFERENCES module_meta(id) ON DELETE CASCADE,
book TEXT NOT NULL, book_number INTEGER NOT NULL, media_json TEXT
);
CREATE TABLE note_entries (
id TEXT PRIMARY KEY, -- "<module id>:<verse_id>"
module_id TEXT NOT NULL,
verse_id INTEGER NOT NULL, -- BBCCCVVV
book INTEGER NOT NULL, chapter INTEGER NOT NULL, verse INTEGER NOT NULL,
title TEXT, content TEXT NOT NULL, -- plain text
verse_refs_json TEXT, -- [end ref], when a note covers a range
last_modified INTEGER, footnotes_json TEXT,
search_text TEXT, record_change_tag TEXT
);
CREATE VIRTUAL TABLE note_fts USING fts5(
title, search_text, content='note_entries', content_rowid='rowid'
);
CREATE TABLE devotional_entries (
id TEXT PRIMARY KEY, -- the module ID
module_id TEXT NOT NULL REFERENCES module_meta(id) ON DELETE CASCADE,
title TEXT NOT NULL, subtitle TEXT, author TEXT,
date TEXT, -- "2026-01-01"
tags TEXT, category TEXT, -- tags comma-separated
series_id TEXT, series_name TEXT, series_order INTEGER,
key_scriptures_json TEXT, summary_json TEXT,
content_json TEXT NOT NULL, footnotes_json TEXT,
related_ids TEXT, created INTEGER NOT NULL, last_modified INTEGER,
search_text TEXT, record_change_tag TEXT,
subscription_id TEXT, is_read_only INTEGER, media_json TEXT
);Highlights
CREATE TABLE highlight_meta (
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
translation_id TEXT NOT NULL, -- offsets are meaningless without this
created INTEGER, last_modified INTEGER
);
CREATE TABLE highlights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ref INTEGER NOT NULL, -- BBCCCVVV
sc INTEGER NOT NULL, -- start character, inclusive
ec INTEGER NOT NULL, -- end character, exclusive
style INTEGER NOT NULL DEFAULT 0, -- 0 fill, 1 solid, 2 dashed, 3 dotted
color TEXT -- hex without #, e.g. "FFE066"; null means the default
);
CREATE TABLE highlight_themes ( -- what a colour and style mean
color TEXT NOT NULL, style INTEGER NOT NULL,
name TEXT NOT NULL, description TEXT,
PRIMARY KEY (color, style)
);Reading plans and quizzes
A quiz names the plan it belongs to through plan_id, which is how the reader knows there are questions available for today’s portion.
CREATE TABLE plans (
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
author TEXT, full_description TEXT,
duration INTEGER NOT NULL, readings_per_day INTEGER
);
CREATE TABLE plan_days (
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
day INTEGER NOT NULL,
readings_json TEXT NOT NULL, -- [{ "sv": ..., "ev": ... }]
PRIMARY KEY (plan_id, day)
);
CREATE TABLE quiz_modules (
id TEXT PRIMARY KEY, plan_id TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, questions_per_reading INTEGER,
age_groups_json TEXT NOT NULL -- [{ "id": ..., "label": ..., "ageRange": ... }]
);
CREATE TABLE quiz_questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
quiz_module_id TEXT NOT NULL REFERENCES quiz_modules(id) ON DELETE CASCADE,
day INTEGER NOT NULL, sv INTEGER NOT NULL, ev INTEGER NOT NULL,
age_group TEXT NOT NULL, question_index INTEGER NOT NULL,
question_json TEXT NOT NULL, answer_json TEXT NOT NULL,
theme TEXT NOT NULL, christ_focused INTEGER NOT NULL DEFAULT 0,
references_json TEXT, cross_references_json TEXT,
UNIQUE(quiz_module_id, day, sv, ev, age_group, question_index)
);Common mistakes
| Symptom | Cause |
|---|---|
| Module will not import | Compressed with a zlib or gzip header. Use wbits=-15. |
| Rejected on iPhone or iPad as containing another module’s rows | The file name does not match the module ID. Name it <module id>.lamp. |
| Imports as the wrong type — a commentary or notes module shows up as a dictionary | No module_format row, so the app guessed the type from the other tables. Add the row, and use the tables for the type you mean. |
| Wrong words tagged or highlighted | Character offsets counted by hand. Derive them from the text. |
| Verses out of order or missing | ref not in BBCCCVVV form — two digits of book, three of chapter, three of verse. |
| Search finds nothing | The full-text index was never filled. Finish by rebuilding it, as in INSERT INTO verses_fts(verses_fts) VALUES('rebuild'). Only verses_fts has a trigger to fill it as rows arrive; for note_fts and book_sections_fts the rebuild is the only way. |
| Highlights land on the wrong text | translation_id names a translation whose wording differs from the one the offsets were taken against. |
Next
For the JSON shape of each module type — the schemas the source documents validate against — see Authoring modules. To get the sample modules, schemas and table definitions as files, see Downloads.