6.5 KiB
Plan: Startup Database Failure — Diagnostics and Degraded Mode
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:344,:363-454), 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/Copy Details, "Save a Copy of My Data" (shareDatabaseFile()), and "Start Over" (resetDatabase(), which copies the file aside before deleting it).- API reads work without an identity:
getHeaders()(src/libs/endorserServer.ts:430) 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
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() already resolves the directory on
Android and iOS.
Part B: Degraded mode
- 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:1071-1136) returnsdefaultSettingsor the caller'sdefaults(often{}) on its early-return and error paths, soapiServercan be undefined;HomeViewworks around this on its own. - 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
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
- 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.
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
- 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.