8.1 KiB
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:
- A failure report reaches the developers without the user composing an email, and it carries enough to identify the cause on the first report.
- The report is sent only with the user's consent and holds no keys, seed phrases, or contact data.
- A user whose database is down can browse public content (feed, projects, claims) instead of being held on the recovery screen.
- Every action that needs the database or an identity is visibly disabled with the real reason, and points to the recovery screen.
- 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 throwsFailed to initialize database (step: <step>): <cause>.src/utils/startupError.tsrecognizes that error by its message and hands it toStartViewthroughsessionStorage.- Three places catch it and redirect to
/start: the routererrorHandlerandbeforeEachguard (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,/testand/logslands on/start. StartViewshows 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
secretandaccountstables (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
migrationstable 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
apiServerin every settings fallback.$accountSettings()(src/utils/PlatformServiceMixin.ts) returns the caller'sdefaults(often{}) on its early-return and error paths, soapiServercan be undefined;HomeViewworks 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
REINDEXor.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
apiServerin all$accountSettings()fallbacks. - B4. Reorder the recovery screen.
- C1. (Optional) Preferences-backed identity with import on recovery.
History
- 2026-10-10: commit
d6ccf0e4added the failed step and cause to the startup error, thestartupError.tshand-off toStartView, and the recovery screen (Try Again, Email/Copy Details, Save a Copy, Start Over). - 2026-10-10: commit
f41a807afixed the root cause (seePLAN-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
d2cdd578added 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.