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

125 lines
6.5 KiB
Markdown

# 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).