Files
crowd-funder-for-time-pwa/doc/CHANGES/history/PLAN-startup-db-fix-crash.md
T

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: isEncryption defaults to true, so on every launch the plugin created an Android Keystore master key and opened an EncryptedSharedPreferences file (sqlite_encrypted_shared_prefs) to hold a database passphrase. If that step throws, the plugin fails to load and every createConnection call is rejected.
  • iOS: the database location defaults to Documents, not the configured Library/CapacitorDatabase.
  • Electron: the plugin reads plugins.CapacitorSQLite directly, 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 to Library/CapacitorDatabase would 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_prefs out with run-as app.timesafari.app, uninstall (which deletes the Keystore key), reinstall, put the copied shared_prefs back, 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:processDebugResources builds, 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_prefs captured 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.json set iosDatabaseLocation to Library/CapacitorDatabase under the correct CapacitorSQLite key (commit 7b4f9d65).
  • 2025-08-14: the config moved to capacitor.config.ts under the key CapSQLite, which the plugin does not read (commit a2840675). Plugin encryption setup ran on Android from then on, and iOS databases moved to the default Documents location.
  • 2026-10-10: commit d6ccf0e4 added 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).