# Plan: Startup Database Failure — Diagnostics and Degraded Mode ## Status Closed without building Parts A–C. The startup failure this plan prepared for had a root cause, and the fix in `PLAN-startup-db-fix-crash.md` removed it: the SQLite plugin's configuration key, migration recording, and Android backup rules. A user who saw the error on every launch started normally after installing a build with that fix. The recovery screen from commit d6ccf0e4 remains the answer for any other database failure. Parts A–C below are kept as a design to pick up if startup database failures are reported again; nothing in them has been built. ## Goal When the local database cannot be opened, a user should (1) get a useful report to the developers with one tap, and (2) keep using whatever parts of the app do not need local storage, with a clear path to recovery. Requirements: 1. A failure report reaches the developers without the user composing an email, and it carries enough to identify the cause on the first report. 2. The report is sent only with the user's consent and holds no keys, seed phrases, or contact data. 3. A user whose database is down can browse public content (feed, projects, claims) instead of being held on the recovery screen. 4. Every action that needs the database or an identity is visibly disabled with the real reason, and points to the recovery screen. 5. Recovery steps never delete the database without keeping a copy. ## Startup failure behavior - `CapacitorPlatformService._initialize()` (`src/services/platforms/CapacitorPlatformService.ts`) opens the database in three steps: "create connection", "open database", "run migrations". A failure throws `Failed to initialize database (step: ): `. - `src/utils/startupError.ts` recognizes that error by its message and hands it to `StartView` through `sessionStorage`. - Three places catch it and redirect to `/start`: the router `errorHandler` and `beforeEach` guard (`src/router/index.ts`), and the Vue global error handler (`src/main.common.ts`). - The guard calls `retrieveAccountDids()` for every route except the identity-setup routes, so with the database down every route except `/account`, `/test` and `/logs` lands on `/start`. - `StartView` shows the recovery screen: Try Again, Email Details, Copy Details, "Save a Copy of My Data" (`shareDatabaseFile()`), and "Start Over" (`resetDatabase()`, which copies the file aside before deleting it). When the browser cannot run WebAssembly, the screen says so and points to the settings that block it. - API reads work without an identity: `getHeaders()` (`src/libs/endorserServer.ts`) omits the Authorization header when there is no DID. - Every path that creates, saves, or signs with an identity reads the `secret` and `accounts` tables (`src/libs/util.ts`: `saveNewIdentity`, `retrieveFullyDecryptedAccount`), so no action that needs a DID can work while the database is down. ## Part A: One-tap diagnostics report (not built) Contents of the report: - the startup error message (step and cause) and stack - app version, platform, OS version, device model - Android: WebView version and installer package (`getInstallerPackageName`) - whether a database file existed before the first connection attempt of this process, which shows whether Android restored a backup at install time - whether the SQLite plugin loaded (a plugin load failure surfaces as a `CapacitorSQLitePlugin:` prefix on the cause) - the `migrations` table contents, when the database opens far enough to read it Delivery: a POST to an endpoint on a server the project runs. The recovery screen shows the exact payload and a Send button; the email option stays for users who decline. The database-existed flag needs a native-side or `Filesystem.stat` check before `createConnection`; `databaseDirectoryUri()` resolves the directory on Android and iOS. ## Part B: Degraded mode (not built) - Change the router guard so that a database startup error records the error and lets read-only routes through: home feed, projects, project and claim views, and discovery. Identity-setup routes and anything that writes stay redirected to `/start`. - Show a persistent banner on every page while in degraded mode: "Your device's storage isn't working. You can look around, but you can't act yet. Fix it →", linking to the recovery screen. - Disable buttons that need a DID or the database (give, offer, create project, confirm, invite, contacts) with that same reason, instead of the "someone must register you" text an unregistered user sees. - Default `apiServer` in every settings fallback. `$accountSettings()` (`src/utils/PlatformServiceMixin.ts`) returns the caller's `defaults` (often `{}`) on its early-return and error paths, so `apiServer` can be undefined; `HomeView` works around this on its own. This gap exists independently of degraded mode. (Moved to PLAN.md) - Order the recovery screen as a ladder: Try Again → Send Diagnostics → Save a Copy → Start Over. ## Part C (optional): An identity that does not need SQLite (not built) Key generation is pure (`generateSeed` → `deriveAddress` → `newIdentifier`). In degraded mode the app could generate an identity, require the user to write down the seed phrase, and keep the mnemonic in `@capacitor/preferences` (installed, and independent of SQLite). The user would then have a DID someone can register. When the database opens again, the app imports the identity from Preferences and deletes the Preferences copy. Trade-off: the mnemonic sits unencrypted in Preferences, about the same protection as the database's secret stored next to the data it encrypts. Worth building only if startup failures keep being reported with the plugin's encryption setting (`androidIsEncryption: false`) in effect. ## Open questions These stay open for whoever picks up Parts A–C. - Which server and endpoint receive diagnostics, and how long reports are kept. - Whether to quarantine a corrupt database automatically ("file is not a database", "malformed") and start fresh. Automatic quarantine trades a visible failure for a silent switch to a new identity, and some "malformed" databases are recoverable with `REINDEX` or `.recover`; the manual Start Over button covers this case. - Whether Android backup should include `databases/`. Including it restores a user's identity on a new phone; it also gives the Google backup a copy of the keys together with the secret that decrypts them. The backup rules (`android/app/src/main/res/xml/backup_rules.xml`, `data_extraction_rules.xml`) include it and exclude only the plugin's encrypted preference files. ## Rejected - An IndexedDB (absurd-sql) database as a fallback inside the Capacitor WebView: untested there, and two stores that both hold keys risk losing an identity when they disagree. ## Checklist Parts A–C are deferred, not scheduled. - [ ] A1. Decide the diagnostics endpoint and payload retention. - [ ] A2. Capture "database file existed before first connection" during startup. - [ ] A3. Build the report payload and the Send Diagnostics button with preview. - [ ] B1. Let read-only routes through the router guard on a database startup error. - [ ] B2. Degraded-mode banner and disabled-action reasons. - [ ] B3. Default `apiServer` in all `$accountSettings()` fallbacks. - [ ] B4. Reorder the recovery screen. - [ ] C1. (Optional) Preferences-backed identity with import on recovery. ## History - 2026-10-10: commit d6ccf0e4 added the failed step and cause to the startup error, the `startupError.ts` hand-off to `StartView`, and the recovery screen (Try Again, Email/Copy Details, Save a Copy, Start Over). - 2026-10-10: commit f41a807a fixed the root cause (see `PLAN-startup-db-fix-crash.md`) and wrote this plan. Tagged 1.4.8; the CHANGELOG records it under 1.4.9 as "Startup DB error for new installations". - 2026-10-17: commit d2cdd578 added the WebAssembly explanation to the recovery screen (CHANGELOG 1.4.10). - After 1.4.9: a user who had the startup error on every launch reported normal startup on a build with the fix. The plan was closed with Parts A–C unbuilt.