You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The current DB delivery architecture has a fundamental coupling problem: content updates require both an R2 upload AND an eas update because scripture.db is tracked in git and bundled into the JS update. This means:
Every content change triggers a full JS bundle push (unnecessary)
Forgetting either step leaves users on stale content
scripture.db is ~94MB tracked in git, approaching GitHub's 100MB limit
Decision: R2 is the single source of truth for scripture.db. The JS bundle never carries the DB. Two separate CI workflows handle content vs. code deploys independently.
Architecture After This Epic
content/** changes → content.yml CI → build DB → validate → delta → R2 upload → done
app/** changes → app.yml CI → lint/test → eas update → done
App first launch: no DB on device → show download screen → download from R2 → open app
App subsequent launches: ContentUpdater checks R2 manifest in background (already built)
Implementation Order (do in this sequence — each step is independently deployable)
Step 1 — Build first-launch download screen
This must exist BEFORE the bundled DB is removed, so there's a fallback.
New file: app/src/screens/DbDownloadScreen.tsx
A full-screen component shown in App.tsx when initDatabase() detects no DB on device. Uses ContentUpdater.downloadFullDb() to fetch from R2. Shows:
Companion Study logo / title
"Setting up your library..." subtitle
Progress bar (gold, #bfa050)
Download size / percentage
Error state with retry button
The screen must hook into ContentUpdater.downloadFullDb() — that method already handles download, checksum verification, and writing to the SQLite directory. It just needs a progress callback added.
Modify: app/src/services/ContentUpdater.ts
Add optional onProgress?: (pct: number) => void callback param to downloadFullDb(). The existing FileSystem.downloadAsync doesn't support progress natively — use FileSystem.createDownloadResumable instead, which has a progressCallback. Pass bytes downloaded / total bytes as 0–100 percentage.
Modify: app/src/db/database.ts
Change initDatabase() to return an enum/status instead of throwing, so App.tsx can decide whether to show the download screen or proceed normally:
exporttypeDbInitStatus='ready'|'needs_download';exportasyncfunctioninitDatabase(): Promise<DbInitStatus>{if(Platform.OS==='web'){ ... return'ready';}constdbPath=`${FileSystem.documentDirectory}SQLite/scripture.db`;constinfo=awaitFileSystem.getInfoAsync(dbPath);if(!info.exists||!info.size||info.size<1000){return'needs_download';// Caller shows download screen}// DB exists — open it (ContentUpdater handles hash check in background)db=awaitSQLite.openDatabaseAsync('scripture.db');awaitdb.execAsync('PRAGMA journal_mode=WAL');return'ready';}
Remove copyAssetDatabaseIfNeeded() entirely. Remove the EXPECTED_CONTENT_HASH / db-manifest.json require at the top of the file — that mechanism is no longer needed since the DB isn't bundled.
Modify: app/App.tsx
Change the init flow:
const[dbStatus,setDbStatus]=useState<'loading'|'needs_download'|'ready'>('loading');useEffect(()=>{asyncfunctioninit(){try{awaitScreenOrientation.lockAsync(...);conststatus=awaitinitDatabase();awaitinitUserDatabase();awaituseSettingsStore.getState().hydrate();awaituseAuthStore.getState().hydrate();awaitusePremiumStore.getState().hydrate();pruneEvents(90);setDbStatus(status);// 'ready' or 'needs_download'}catch(e){console.error('Init error:',e);setDbStatus('needs_download');// Fail safe — show download screen}}init();},[]);
When dbStatus === 'needs_download', render <DbDownloadScreen onComplete={() => setDbStatus('ready')} /> instead of the normal app tree. DbDownloadScreen calls onComplete() after successful download, which transitions to 'ready' and renders the full app.
actions/setup-python@v5 with python-version: '3.11'
pip install boto3
python _tools/build_sqlite.py
python _tools/validate_sqlite.py
python _tools/generate_delta.py (generates + uploads delta to R2, updates manifest)
python _tools/upload_to_r2.py (uploads full DB + updates manifest)
Note: generate_delta.py must run BEFORE upload_to_r2.py because it downloads the currently-deployed DB (old version) from R2 to diff against. After upload_to_r2.py runs, the manifest points to the new version.
No eas update in this workflow. No DB committed back to git.
Step 3 — Write app.yml GitHub Actions workflow
New file: .github/workflows/app.yml
Close/delete PR #1311 (the broken workflow we already opened) before merging this.
Triggers: push to master, paths: app/** (excluding app/assets/scripture.db which is gitignored anyway)
Steps:
actions/checkout@v4
actions/setup-node@v4 with node-version: '20', cache npm, cache-dependency-path app/package-lock.json
cd app && npm ci
npm run lint (fails workflow on lint errors)
npm install -g eas-cli@latest
eas update --branch production --message "App update from ${{ github.sha }}" --non-interactive
Required secret: EXPO_TOKEN (expo.dev → Account Settings → Access Tokens → create one named github-actions)
No DB rebuild. No R2 upload.
Step 4 — Remove DB from EAS bundle
Modify: app/app.json
Confirm assets array doesn't include scripture.db. It currently shows [] so this may already be clean. Double-check.
Delete: app/scripts/setup.js
This script's only purpose is copying scripture.db into app/assets/ for Metro bundling. With the DB no longer bundled, it's dead code. Also remove from package.json scripts (setup, prestart, start, dev, android, ios, web all call it).
Delete: app/scripts/pre-eas-update.js
Same — exists solely to verify/copy DB before eas update. Dead code.
Modify: app/package.json scripts
Remove node scripts/setup.js && prefix from all scripts. The update script becomes just:
"update": "eas update --branch production --message"
Keep: app/assets/db-manifest.json
This file can stay for now — it's tiny and harmless. Once database.ts no longer reads from it (Step 1), it becomes vestigial but won't cause harm. Can be cleaned up later.
Step 5 — Remove scripture.db from git
Run locally after all the above is merged and verified working:
# Add to root .gitignoreecho"scripture.db">> .gitignore
# Remove from git tracking (keeps local file)
git rm --cached scripture.db
# Commit
git add .gitignore
git commit -m "chore: remove scripture.db from git tracking — delivered via R2"
git push
Note: This does NOT delete the local file — you still need it locally to run build_sqlite.py and upload_to_r2.py. It just stops git from tracking changes to it.
Files Changed Summary
File
Action
app/src/screens/DbDownloadScreen.tsx
CREATE
app/src/services/ContentUpdater.ts
MODIFY — add progress callback to downloadFullDb()
app/src/db/database.ts
MODIFY — return DbInitStatus, remove bundled DB copy logic
Context
The current DB delivery architecture has a fundamental coupling problem: content updates require both an R2 upload AND an
eas updatebecausescripture.dbis tracked in git and bundled into the JS update. This means:scripture.dbis ~94MB tracked in git, approaching GitHub's 100MB limitDecision: R2 is the single source of truth for
scripture.db. The JS bundle never carries the DB. Two separate CI workflows handle content vs. code deploys independently.Architecture After This Epic
App first launch: no DB on device → show download screen → download from R2 → open app
App subsequent launches: ContentUpdater checks R2 manifest in background (already built)
Implementation Order (do in this sequence — each step is independently deployable)
Step 1 — Build first-launch download screen
This must exist BEFORE the bundled DB is removed, so there's a fallback.
New file:
app/src/screens/DbDownloadScreen.tsxA full-screen component shown in
App.tsxwheninitDatabase()detects no DB on device. UsesContentUpdater.downloadFullDb()to fetch from R2. Shows:#bfa050)The screen must hook into
ContentUpdater.downloadFullDb()— that method already handles download, checksum verification, and writing to the SQLite directory. It just needs a progress callback added.Modify:
app/src/services/ContentUpdater.tsAdd optional
onProgress?: (pct: number) => voidcallback param todownloadFullDb(). The existingFileSystem.downloadAsyncdoesn't support progress natively — useFileSystem.createDownloadResumableinstead, which has aprogressCallback. Pass bytes downloaded / total bytes as 0–100 percentage.Modify:
app/src/db/database.tsChange
initDatabase()to return an enum/status instead of throwing, soApp.tsxcan decide whether to show the download screen or proceed normally:Remove
copyAssetDatabaseIfNeeded()entirely. Remove theEXPECTED_CONTENT_HASH/db-manifest.jsonrequire at the top of the file — that mechanism is no longer needed since the DB isn't bundled.Modify:
app/App.tsxChange the init flow:
When
dbStatus === 'needs_download', render<DbDownloadScreen onComplete={() => setDbStatus('ready')} />instead of the normal app tree.DbDownloadScreencallsonComplete()after successful download, which transitions to'ready'and renders the full app.Step 2 — Write
content.ymlGitHub Actions workflowNew file:
.github/workflows/content.ymlTriggers:
pushtomaster, paths:content/**,_tools/build_sqlite*.py,_tools/validate_sqlite.py,_tools/config.py,_tools/content_writer.py,_tools/upload_to_r2.py,_tools/generate_delta.pySteps:
actions/checkout@v4withfetch-depth: 0actions/setup-python@v5withpython-version: '3.11'pip install boto3python _tools/build_sqlite.pypython _tools/validate_sqlite.pypython _tools/generate_delta.py(generates + uploads delta to R2, updates manifest)python _tools/upload_to_r2.py(uploads full DB + updates manifest)Note:
generate_delta.pymust run BEFOREupload_to_r2.pybecause it downloads the currently-deployed DB (old version) from R2 to diff against. Afterupload_to_r2.pyruns, the manifest points to the new version.Required secrets (add to GitHub repo → Settings → Secrets → Actions):
R2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKET_NAMER2_PUBLIC_URLNo
eas updatein this workflow. No DB committed back to git.Step 3 — Write
app.ymlGitHub Actions workflowNew file:
.github/workflows/app.ymlClose/delete PR #1311 (the broken workflow we already opened) before merging this.
Triggers:
pushtomaster, paths:app/**(excludingapp/assets/scripture.dbwhich is gitignored anyway)Steps:
actions/checkout@v4actions/setup-node@v4withnode-version: '20', cachenpm, cache-dependency-pathapp/package-lock.jsoncd app && npm cinpm run lint(fails workflow on lint errors)npm install -g eas-cli@latesteas update --branch production --message "App update from ${{ github.sha }}" --non-interactiveRequired secret:
EXPO_TOKEN(expo.dev → Account Settings → Access Tokens → create one namedgithub-actions)No DB rebuild. No R2 upload.
Step 4 — Remove DB from EAS bundle
Modify:
app/app.jsonConfirm
assetsarray doesn't includescripture.db. It currently shows[]so this may already be clean. Double-check.Delete:
app/scripts/setup.jsThis script's only purpose is copying
scripture.dbintoapp/assets/for Metro bundling. With the DB no longer bundled, it's dead code. Also remove frompackage.jsonscripts (setup,prestart,start,dev,android,ios,weball call it).Delete:
app/scripts/pre-eas-update.jsSame — exists solely to verify/copy DB before
eas update. Dead code.Modify:
app/package.jsonscriptsRemove
node scripts/setup.js &&prefix from all scripts. Theupdatescript becomes just:Keep:
app/assets/db-manifest.jsonThis file can stay for now — it's tiny and harmless. Once
database.tsno longer reads from it (Step 1), it becomes vestigial but won't cause harm. Can be cleaned up later.Step 5 — Remove
scripture.dbfrom gitRun locally after all the above is merged and verified working:
Note: This does NOT delete the local file — you still need it locally to run
build_sqlite.pyandupload_to_r2.py. It just stops git from tracking changes to it.Files Changed Summary
app/src/screens/DbDownloadScreen.tsxapp/src/services/ContentUpdater.tsdownloadFullDb()app/src/db/database.tsDbInitStatus, remove bundled DB copy logicapp/App.tsxneeds_downloadstate.github/workflows/content.yml.github/workflows/app.yml.github/workflows/content-deploy.ymlapp/scripts/setup.jsapp/scripts/pre-eas-update.jsapp/package.json.gitignore(root)scripture.dbscripture.dbgit rm --cached)Key Design Decisions
database.tsno longer readsdb-manifest.json— the hash check against a bundled expected hash is meaningless once the DB isn't bundledContentUpdaterowns ALL DB acquisition: first download AND subsequent updates. Same code path.generate_delta.pyruns beforeupload_to_r2.pyin CI — delta diffs old-vs-new, upload then replaces the manifest's current_version[skip ci]trick from the old workflow is gone — no DB committed back to git at allGitHub Secrets Required
In GitHub repo → Settings → Secrets and variables → Actions:
EXPO_TOKEN— from expo.dev → Account Settings → Access TokensR2_ACCOUNT_ID— Cloudflare dashboardR2_ACCESS_KEY_ID— R2 API tokenR2_SECRET_ACCESS_KEY— R2 API token secretR2_BUCKET_NAME—companionstudybucketR2_PUBLIC_URL—https://contentcompanionstudy.comThese are already in your local
.envfile — just copy them to GitHub.