Files
crowd-funder-for-time-pwa/doc/CHANGES/PLAN-startup-db-failure-recovery.md
T

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:

  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: <step>): <cause>.
  • 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: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, /test and /logs lands on /start.
  • StartView shows 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 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

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() 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 apiServer in every settings fallback. $accountSettings() (src/utils/PlatformServiceMixin.ts:1071-1136) returns defaultSettings or the caller's defaults (often {}) on its early-return and error paths, so apiServer can be undefined; HomeView works 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 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.

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 apiServer in all $accountSettings() fallbacks.
  • B4. Reorder the recovery screen.
  • C1. (Optional) Preferences-backed identity with import on recovery.