# Database pair and launch integrity
The app bundles a database and its certified manifest in two profiles; a licence gate decides which may ship; at launch the app hashes the database and refuses scripture on a mismatch.
- Page:
- Updated: 2026-09-25
- Verified against: `417c92f` on 2026-09-25
- From the Sri Guru Granth Sahib Ji — Knowledge Base wiki. Sri Guru Granth Sahib Ji is quoted verbatim, cited by Ang, and never altered.
---
The app carries the scripture with it. Two files travel together — `sggs-ios.sqlite` and
`sggs-ios.manifest.json` — and the manifest's checksums are what every gate, from the archive
script to the app's own launch, verifies against. The poster follows a release from the vendor sync
to the ledger, then the two ladders that run on the device.

## Two profiles, one builder
`pipeline/build_ios_db.py` (owned by sggs-data, vendored at the dataset's commit) derives the iOS
database from the pinned `db/sggs.sqlite` and writes the manifest beside it:
| Profile | Contents | May ship publicly? |
|---|---|---|
| `personal` | every table, including the English translations and `fts_en` | only with `LICENSED: true` in `ios/Resources/TRANSLATION-LICENSE.md` — the Khalsa translation is personal-use until licensed in writing |
| `public` | Gurmukhi-only: the translation layer dropped, everything else verbatim (timing, banis, insights included) | yes — the profile the App Store build uses |
The manifest records the profile, the database's `db_sha256`, the `scripture_sha256` (a checksum
over the scripture rows alone, the same definition sggs-data's fingerprint uses), whether English
is bundled, the byte size and the tables. On a developer's machine the pair is git-ignored for the
database and tracked for the manifest; `make ios-db-check` proves they agree after a branch switch
and `make ios-db-repair` re-derives the database without touching git.
## The licence gate
```bash
#!/usr/bin/env bash
# check_release_license.sh — the mechanical NOTICE.md hard-gate for any PUBLIC iOS release.
#
# Fails (exit 1) if:
# (a) the manifest says en_bundled=true AND the licence attestation file
# (ios/Resources/TRANSLATION-LICENSE.md, override with $SGGS_LICENSE_ATTESTATION)
# does not contain the line `LICENSED: true` — the Khalsa English translation is
# PERSONAL, LOCAL, NON-COMMERCIAL use only until licensed in writing; a public /
# TestFlight / App Store build must either be built with `--profile public` or
# carry a filled-in attestation;
# (b) scripture_sha256 differs from the certified corpus value — scripture must be
# byte-identical to the proven corpus, always;
# (c) the DB artifact's sha256 doesn't match its manifest;
# (d) any golden-vector suite recorded in contract/_meta.json is missing or empty;
# (e) the bundled DB carries the Nitnem registry with NON-SGGS text (extra_lines) and
# either that table has an English column (never allowed) or the scholar review
# is not attested with `REVIEWED: true` in ios/Resources/NITNEM-REVIEW.md
# (override with $SGGS_NITNEM_ATTESTATION) — Sri Dasam Granth / Ardaas text is
# not covered by the SGGS reconcile proof. Unreviewed text is a WARNING for
# TestFlight builds (the app labels it "under review") and a hard FAIL when
# SGGS_RELEASE_CHANNEL=appstore (App Store submission).
#
# Usage: pipeline/check_release_license.sh [manifest] [db]
# defaults: ios/Resources/sggs-ios-public.manifest.json + its sibling .sqlite
# (pass the personal manifest to CONFIRM it is blocked — that's the negative test)
```
*`gurbani-soul-ios/pipeline/check_release_license.sh`, lines 1–25, read at `a386422`: *
Source: [`pipeline/check_release_license.sh`](https://github.com/Algorythmos-AI/gurbani-soul-ios/blob/main/pipeline/check_release_license.sh) in gurbani-soul-ios.
The gate runs on the **exact artifact** the archive stages, never on the developer's pair. It
fails a public build that bundles English without the attestation, a database whose
`scripture_sha256` differs from the certified corpus value, an artifact that does not hash to its
manifest, a missing golden suite, or — for an App Store channel — Nitnem's non-SGGS text without
the scholar's `REVIEWED: true`.
## The archive, gate by gate
`ios/tools/testflight_archive.sh` stops at the first failure:
1. **Refuse a dirty tree**, require Xcode at or above Apple's SDK floor, wait for green CI on the
commit, check the version invariants and the ledger (`--strict` when uploading).
2. **Stage** the database for the chosen profile into `ios/App/build/stage--/`.
3. **Licence gate** on that artifact.
4. **XcodeGen** a temporary project from a sibling spec that differs from `project.yml` in exactly
three lines (the name and the two database resource paths → staging); the developer's own
project is never repointed and neither project is committed.
5. **Archive** the Release configuration with the Team ID and build number from the command
line, automatic signing; **export** for App Store Connect (with `UPLOAD=1` the export *is* the
upload).
6. **Prove** what was built: the Info.plist version and build, and the manifest inside the `.app`,
equal what was staged; then **record** the build in the ledger.
```bash
#!/usr/bin/env bash
# testflight_archive.sh — build the TestFlight / App Store candidate the *only* sanctioned way.
#
# What it does, in order (every step is a gate; the script stops on the first failure):
# 1. Derives the iOS DB for the chosen PROFILE from db/sggs.sqlite (scripture checksum proven
# by pipeline/build_ios_db.py) into a STAGING directory under ios/App/build/. The developer's
# ios/Resources/sggs-ios.sqlite + manifest pair is never read, written or restored.
# 2. Runs pipeline/check_release_license.sh on THAT artifact — a public build must be
# Gurmukhi-only, or the English translation must be attested LICENSED: true.
# 3. Generates SGGS-TestFlight.xcodeproj with XcodeGen from a temporary sibling spec that is
# project.yml with exactly three lines changed (name + the two DB resource paths → staging).
# Your own SGGS.xcodeproj is not repointed; neither project is ever committed.
# 4. Archives the Release configuration with the Team ID and build number passed on the
# command line (project.yml is not edited), automatic signing.
# 5. Exports for App Store Connect; with SGGS_UPLOAD=1 the export *is* the upload.
# 6. Proves what was built: the Info.plist version/build and the manifest INSIDE the .app.
#
# Usage (macOS with Xcode ≥ 16, XcodeGen, python3; `make dataset` first; run from anywhere):
# SGGS_TEAM_ID=ABCDE12345 SGGS_BUILD_NUMBER=4 ios/tools/testflight_archive.sh
# SGGS_TEAM_ID=… SGGS_BUILD_NUMBER=5 SGGS_UPLOAD=1 ios/tools/testflight_archive.sh
#
# Environment:
# SGGS_TEAM_ID (required) Apple Developer Team ID.
# SGGS_BUILD_NUMBER (required) CFBundleVersion for this upload. Must be > every build already
# uploaded for this MARKETING_VERSION (App Store Connect rejects reuse).
# SGGS_DB_PROFILE public (default) | personal. `personal` bundles the English layer and is
# accepted only with LICENSED: true in ios/Resources/TRANSLATION-LICENSE.md.
# SGGS_RELEASE_CHANNEL testflight (default) | appstore. A TestFlight binary and an App Store binary are
# the same upload, so the channel is declared up front: `appstore` makes
# pipeline/check_release_license.sh REQUIRE the scholar review of the Nitnem
# non-SGGS text (ios/Resources/NITNEM-REVIEW.md → REVIEWED: true) and requires
# the in-app "under review" label to be off. Only a build recorded with
# channel=appstore may be submitted (make appstore-preflight checks the ledger).
# SGGS_UPLOAD 1 → export destination "upload" (straight to App Store Connect).
# 0 (default) → write an .ipa under ios/App/build/ for Transporter.
# SGGS_ASC_KEY_PATH / SGGS_ASC_KEY_ID / SGGS_ASC_ISSUER_ID
# App Store Connect API key (all three, or none). Needed for CI / an
# unattended upload; a logged-in Xcode account suffices locally.
# SGGS_SKIP_CI_CHECK 1 → with SGGS_UPLOAD=1, skip waiting for green CI on the commit (offline /
# pipeline-outage use only; the candidate record notes it).
# SGGS_ARCHIVE_DRY_RUN 1 → stop after step 3 (stage + gate + generate); nothing is signed,
# archived or uploaded. Used by ios/tests/test_ios_archive.py.
#
# Tracked files this script may touch: only — and only with SGGS_UPLOAD=1 — the append-only
# ios/testflight-builds.json ledger, which you then commit with the release. It runs no git
# command that changes the working tree. Everything else it writes is git-ignored and lives under
# ios/App/build/ (staging, archive, export, lock) or is the temporary ios/App/SGGS-TestFlight.yml
# + SGGS-TestFlight.xcodeproj. NEVER commit ios/App/build/ or ios/Resources/*.sqlite.
```
*`gurbani-soul-ios/ios/tools/testflight_archive.sh`, lines 1–48, read at `a386422`: *
Source: [`ios/tools/testflight_archive.sh`](https://github.com/Algorythmos-AI/gurbani-soul-ios/blob/main/ios/tools/testflight_archive.sh) in gurbani-soul-ios.
## On the device: the launch-integrity ladder
`LaunchIntegrity` is the iOS analogue of `/api/health`: a fail-closed self-test before any
scripture is shown.
```swift
static func run(corpus: CorpusActor, cacheSuiteName: String? = nil) async -> IntegrityReport {
#if DEBUG
LaunchClock.mark(.integrityStart)
#endif
var checks: [IntegrityReport.Check] = []
let defaults = cacheSuiteName.flatMap { UserDefaults(suiteName: $0) } ?? .standard
let path = corpus.dbPath
let expected = expectedSha()
let attrs = fileFingerprint(path: path)
let bundleVersion = bundleVersionString()
let stored = defaults.data(forKey: cacheKey).flatMap { try? JSONDecoder().decode(DBFingerprint.self, from: $0) }
let cached = cacheHit(stored: stored, expectedSha: expected,
fileSize: attrs?.size, fileMtimeNs: attrs?.mtimeNs, bundleVersion: bundleVersion)
let sha: String
if cached {
sha = expected ?? "" // full verify already proved this exact file
} else {
#if DEBUG
// Fault injection for the device checklist (D11): hold a cold verify open so a force
// quit can land during it. Debug only; check_no_test_hooks.sh keeps it out of releases.
if let hold = ProcessInfo.processInfo.environment["SGGS_UITEST_HOLD_VERIFY"].flatMap(Double.init) {
try? await Task.sleep(for: .seconds(min(hold, 15)))
}
#endif
// Hash the ~100 MB DB OFF the main thread (this is awaited from a @MainActor caller).
sha = await Task.detached(priority: .userInitiated) { sha256(ofFileAt: path) ?? "" }.value
}
#if DEBUG
LaunchClock.mark(.integrityHashed)
#endif
let hashOK = expected != nil && sha == expected
checks.append(.init(name: "db_sha256 matches manifest", passed: hashOK))
if hashOK && !cached, let expected, let attrs {
let fp = DBFingerprint(dbSha256: sha, fileSize: attrs.size, fileMtimeNs: attrs.mtimeNs,
bundleVersion: bundleVersion, manifestSha: expected)
if let data = try? JSONEncoder().encode(fp) { defaults.set(data, forKey: cacheKey) }
}
if !hashOK { defaults.removeObject(forKey: cacheKey) } // never cache a failure
// Structural checks run on EVERY launch (cached or not) — cheap defence in depth.
// Mool Mantar prefix on Ang 1
let mool = (try? await corpus.ang(1).lines.first?.gurmukhi)?
.hasPrefix("ੴ ਸਤਿ ਨਾਮੁ ਕਰਤਾ ਪੁਰਖੁ ਨਿਰਭਉ ਨਿਰਵੈਰੁ") ?? false
checks.append(.init(name: "Mool Mantar present", passed: mool))
// FTS works
let fts = ((try? await corpus.search("ਨਾਮੁ", mode: "gurmukhi", limit: 1, offset: 0))?.results.isEmpty == false)
checks.append(.init(name: "FTS search works", passed: fts))
// verify engine self-test → VERIFIED_EXACT (matched line id 5)
let verdict = (try? await corpus.verify("ਸੋਚੈ ਸੋਚਿ ਨ ਹੋਵਈ ਜੇ ਸੋਚੀ ਲਖ ਵਾਰ", ang: 1))?.verdict ?? ""
checks.append(.init(name: "Verify engine sane", passed: verdict.hasPrefix("VERIFIED_EXACT")))
#if DEBUG
LaunchClock.mark(.integrityDone)
#endif
return IntegrityReport(ok: checks.allSatisfy { $0.passed }, dbSha256: sha,
expectedSha256: expected, sqliteVersion: corpus.sqliteVersion,
checks: checks, usedCachedHash: cached)
}
```
*`gurbani-soul-ios/ios/App/Sources/Data/LaunchIntegrity.swift`, lines 91–152, read at `a386422`: *
Source: [`ios/App/Sources/Data/LaunchIntegrity.swift`](https://github.com/Algorythmos-AI/gurbani-soul-ios/blob/main/ios/App/Sources/Data/LaunchIntegrity.swift) in gurbani-soul-ios.
- The SHA-256 of the bundled database is compared with the manifest's `db_sha256`; if it holds,
the database is bit-identical to the proven corpus and every invariant holds by construction.
- After one **full** streaming verify the app stores a fingerprint (the sha, the file size and
modification time, the bundle version, the manifest sha). At the next launch, if every field
still matches, the ~100 MB re-hash is skipped and only the cheap structural checks run; **any**
mismatch — a fresh install, an update, a repaired bundle, a manifest change — forces a full
re-verify, and a failure is never cached.
- If anything fails the app refuses to present scripture ("Scripture integrity check failed") and
shows the report under More → About → Integrity.
- The threat model is **accidents** — corruption, a truncated copy, a bad build — not an attacker:
anyone who can rewrite a file inside the signed bundle can rewrite the cache too; anti-tamper is
the code-signing layer's job.
## Bookmarks: a separate store with its own ladder
Saved verses live in a **separate, writable SwiftData store**, never in the read-only corpus
database, and store the verbatim Gurmukhi (no saroop markup). The schema is versioned from the
first TestFlight build so a later change ships as a real migration stage:
```swift
enum SavedLineSchemaV1: VersionedSchema {
static let versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [SavedLine.self] }
}
```
*`gurbani-soul-ios/ios/App/Sources/Persistence/SavedLine.swift`, lines 29–32, read at `a386422`: *
Source: [`ios/App/Sources/Persistence/SavedLine.swift`](https://github.com/Algorythmos-AI/gurbani-soul-ios/blob/main/ios/App/Sources/Persistence/SavedLine.swift) in gurbani-soul-ios.
Opening the store climbs its own ladder: the versioned schema with its migration plan first; if
that fails, a retry; the store is **destroyed and recreated only on a second consecutive failure**,
so a transient error never costs a reader their bookmarks. Nitnem progress is a separate JSON file
in the App Group with a frozen v1 schema ([Nitnem for engineers](https://docs.gurbanisoul.com/ios/nitnem-for-engineers/)).