6.5 KiB
Fix: Startup Database Error on Android
Symptom
On some Android installs from the Play Store, the app showed a "Startup Error" on every launch:
[CapacitorPlatformService] Failed to initialize database
Reinstalling did not help. The failure never appeared on developer devices installed through adb or Android Studio.
Causes
1. The SQLite plugin ignored its configuration (main cause)
capacitor.config.ts listed the SQLite settings under the key CapSQLite.
The plugin registers as CapacitorSQLite and reads only that key, so every
setting under CapSQLite was ignored and the plugin defaults applied:
- Android:
isEncryptiondefaults to true, so on every launch the plugin created an Android Keystore master key and opened anEncryptedSharedPreferencesfile (sqlite_encrypted_shared_prefs) to hold a database passphrase. If that step throws, the plugin fails to load and everycreateConnectioncall is rejected. - iOS: the database location defaults to
Documents, not the configuredLibrary/CapacitorDatabase. - Electron: the plugin reads
plugins.CapacitorSQLitedirectly, so a bundled config with the wrong key would throw at plugin startup.
The Keystore step fails on Android in two ways:
- Restore without the key. Android Auto Backup and device-to-device transfer restore the encrypted preferences file, but Keystore keys never leave the device. The restored file can't be decrypted, so the plugin fails on every launch, and a reinstall from Play restores the same file again.
- Device Keystore bugs. Some manufacturers' Keystore implementations fail without any restore.
The app never used plugin encryption: it opens the database with
encrypted = false and mode "no-encryption", and never stores a passphrase.
The Keystore setup protected nothing.
2. A migration could be applied but not recorded
The migration runner ran each migration's SQL in one transaction and recorded
its name in the migrations table in a second one. If the app died between the
two, the migration ran again on the next launch. Migration 001's
INSERT INTO secret (id, ...) VALUES (1, ...) then failed with
UNIQUE constraint failed: secret.id on every launch. The window is
milliseconds, but once hit the failure is permanent, and Auto Backup carries it
to a reinstall.
Changes
| Area | Change |
|---|---|
capacitor.config.ts, electron/capacitor.config.ts, android/app/src/main/assets/capacitor.config.json |
Key renamed to CapacitorSQLite, so androidIsEncryption: false takes effect and the Keystore step never runs. iosDatabaseLocation set to Documents, the location every iOS install uses. |
capacitor.config.json, electron/capacitor.config.json |
iosDatabaseLocation set to Documents to match. These files already used the correct key. |
src/services/platforms/CapacitorPlatformService.ts |
databaseDirectoryUri() points at Documents on iOS, so "Save a Copy of My Data" and "Start Over" find the real database file. |
src/services/migrationService.ts |
Each migration's INSERT INTO migrations is appended to its own statement batch, which the native plugin runs as one transaction. |
src/db-sql/migration.ts |
Migration 001 uses INSERT OR IGNORE for the secret and settings rows, so a re-run never replaces the existing secret. |
android/app/src/main/AndroidManifest.xml, res/xml/backup_rules.xml, res/xml/data_extraction_rules.xml |
Cloud backup and device transfer exclude the plugin's encrypted preference files. Everything else is backed up, including the database. |
src/test/migrations.test.ts |
Tests for a crash right after a migration commits, and for a re-run of 001 keeping the secret. |
src/test/capacitorConfig.test.ts |
Fails if the plugin config key or the iOS location is wrong. |
Security
Encryption of user keys is unchanged. The identity and seed phrase are encrypted
by the app (simpleEncrypt with the random secret from migration 001) and stored
in accounts. The plugin's encryption flag only controlled whole-database
SQLCipher encryption, which the app never used; the database file was plain
SQLite before and after this change. App sandboxing keeps the file private to
the app on both platforms.
Compatibility with existing installs
- Android: the database has no password whether plugin encryption is on or off, so it opens the same way. The leftover encrypted preferences file is never read again.
- iOS: the database stays in
Documents. Setting the location back toLibrary/CapacitorDatabasewould not be safe: the plugin moves databases into a custom location only when that folder does not exist yet, and installs from between May and August 2025 have that folder with an outdated database in it. - Migrations: existing installs already have 001 recorded, so its edit never runs for them. The recording change affects only migrations that have not run.
Verification
- Reproduced before the fix on an Android emulator: run the app once, copy
shared_prefsout withrun-as app.timesafari.app, uninstall (which deletes the Keystore key), reinstall, put the copiedshared_prefsback, and launch. The startup error appeared. - Unit tests:
pkgx npm run test:unit -- src/test/migrations.test.ts src/test/capacitorConfig.test.ts. The two migration tests fail on the earlier code and pass on the fixed code. - Android resources:
./gradlew :app:processDebugResourcesbuilds, and the merged manifest carries both backup-rule attributes. - After the fix: run the same emulator steps on a fixed build, and also
restore
shared_prefscaptured from a pre-fix build. The app should start normally either way.
Follow-up
The one-tap diagnostics report and a degraded mode for users without a working
database are designed, but not built, in
PLAN-startup-db-failure-recovery.md in this directory.
History
- 2025-05-27:
capacitor.config.jsonsetiosDatabaseLocationtoLibrary/CapacitorDatabaseunder the correctCapacitorSQLitekey (commit7b4f9d65). - 2025-08-14: the config moved to
capacitor.config.tsunder the keyCapSQLite, which the plugin does not read (commita2840675). Plugin encryption setup ran on Android from then on, and iOS databases moved to the defaultDocumentslocation. - 2026-10-10: commit
d6ccf0e4added the failed step and underlying cause to the startup error, made migration 008 tolerate duplicate contact DIDs and labels pointing at deleted contacts, and added the recovery screen. - 2026-10-10: this fix (config key, atomic migration records, backup rules).