This document describes MAIA's account lifecycle flows, the modals involved in each, verification steps, and how multiple family members are kept separate.
Account state lives in three systems:
| System | What it stores |
|---|---|
CouchDB (maia_users) |
User doc: assignedAgentId, kbId, kbName, files[], workflowStage, currentMedications, patientSummary |
| DigitalOcean | Agent (GenAI platform), Knowledge Base, Spaces (S3) files |
| Browser | IndexedDB folder handles (per-user), PouchDB snapshots, sessionStorage flags, maia-state.json in local folder |
Every session begins here when the user is not authenticated.
If discoverUsers() finds entries (from IndexedDB directory handles),
each is displayed as a status card with color-coded border and icon:
| Cloud status | Border color | Icon | Action button |
|---|---|---|---|
ready |
Green | check_circle | SIGN IN / CONTINUE |
loading |
Grey | hourglass_empty | (spinner) |
restore |
Orange | warning | RESTORE |
Each card shows the user's display name, userId, folder name, and action buttons:
- RESTORE button (orange, shown when
cloudStatus === 'restore') - X button to remove from this device
Below the cards, an "Add family member" button starts a fresh new-user flow. If no discovered users exist, a simple text line offers to sign in with a passkey or create a new account.
- GET STARTED button (blue, full width) — calls
handleGetStartedNoPassword(). When restorable users exist, shows a disambiguation dialog first (see below). - Introduction — loaded from
public/welcome.md, rendered via vue-markdown. - Footer links: Privacy | User Guide | FAQ | About — each opens a static HTML
page (
/privacy.html,/User_Guide.html,/faq.html,/about.html) in a new tab. These HTML files live inpublic/and are hand-edited.
Modal: showGetStartedChoiceDialog
When a user clicks GET STARTED and there are restorable (destroyed) users among discovered users, this dialog appears to prevent accidentally creating a new account when the user intended to restore:
- One Restore {name} button per restorable user
- An Add a new family member button for genuinely new accounts
When a truly new user clicks GET STARTED (no knownUsers at all):
Modal: showDevicePrivacyDialog (persistent)
- "Is this a private computer or a shared computer?"
- PRIVATE: Sets
sharedComputerMode = false, proceeds to create session. - SHARED: Sets
sharedComputerMode = true, shows a warning that a passkey will be required, then proceeds.
startTemporarySession() calls POST /api/temporary-session, which
creates a CouchDB user doc and returns a session cookie. The user is now
authenticated and the main ChatInterface loads. The directory handle is
stored in IndexedDB for the new userId.
If sharedComputerMode is true, passkey registration is started
immediately after authentication.
Modal: showAgentSetupDialog (persistent, dismissible via Continue
button or X once agent deployment completes)
The wizard displays a vertical checklist of steps. The first three run in parallel; the last two require user verification.
Stage 1 — Private AI Agent Deployment
- Triggered automatically on ChatInterface mount.
- ChatInterface polls
GET /api/agent-setup-statusevery few seconds. - This endpoint calls
ensureUserAgent()to create a DO agent if none exists. - A countdown timer shows elapsed time (typically 2-5 minutes).
- Complete when
assignedAgentIdexists and agent is deployed. - Status line: "Ready" when done.
Stage 2 — File Upload and Import
- User picks a local folder (via File System Access API) or selects individual files (Safari/other browsers).
- Files are uploaded via
POST /api/files/upload. - If an Apple Health PDF is detected, it becomes the
initialFileand is marked for list/medication extraction after indexing.
Stage 3 — Knowledge Base Indexing
- Triggered by
POST /api/update-knowledge-base. - Server creates or reuses a DO Knowledge Base, copies files into an ephemeral Spaces bucket, and starts an indexing job.
- Server-side polling (every 15s): tracks DO job status, token
counts, and token stability. Completion is detected by:
- DO API job status transitioning to "completed"
- Token-stable detection: tokens > 0 and unchanged for 4+ polls (60s)
- Time-based fallback: 15 minutes elapsed
- Client-side polling (every 15s): reads
kbIndexingStatusfrom CouchDB. Completion fallbacks:backendCompletedflag set by serverinferredComplete: no active job and tokens > 0tokenTimeoutComplete: tokens > 0 and 7 minutes elapsedpureTimeoutComplete: 20 minutes elapsed (catches 0-token case)
- Console logging is state-change-only (logs only when poll state differs from previous poll).
When both Stage 1 (agent) and Stage 3 (indexing) complete, the wizard
enters a guided flow controlled by wizardFlowPhase:
'running' → 'medications' → 'summary' → 'done'
Phase: 'running' → 'medications' (automatic)
The wizard dialog stays open with "Preparing..." spinners on the Current Medications and Patient Summary checklist items while:
- Patient Summary is generated via
/api/generate-patient-summaryand saved to the server. - The subtitle changes to "Preparing health records... Almost done."
- The Continue/X buttons are hidden to prevent premature dismissal.
Once the summary is saved, the wizard dialog closes and the My Stuff dialog opens on the My Lists tab. Lists.vue auto-processes the Apple Health file (if present), extracts category lists, and uses AI to generate Current Medications from the medication records.
The user reviews, edits, and verifies the Current Medications.
Phase: 'medications' → 'summary'
When the user saves/verifies medications, the My Stuff dialog switches to the Patient Summary tab. If a pre-generated summary exists, it is updated with the verified medications. Otherwise, a new summary is generated.
Phase: 'summary' → 'done'
When the user saves or verifies the Patient Summary, the guided flow
completes. The wizard emits 'wizard-complete' and My Stuff stays
open for the user to explore.
Guided Flow Dismissal Handling
If the user closes My Stuff during the guided flow:
- First dismissal: dialog reopens on the same tab (via
nextTick). - Second dismissal in 'medications' phase: skips to 'summary' phase, reopens on Patient Summary tab.
- Second dismissal in 'summary' phase: completes the wizard without summary verification.
guidedFlowDismissCount tracks dismissals per phase and resets on
each phase transition.
wizardFlowPhaseis not persisted to storage — it resets to'done'on page reload.- On reload, a resume watcher checks whether indexing is complete and agent is ready but medications/summary are still pending. If so, it re-enters the appropriate phase and reopens My Stuff.
wizardAutoFlowflag is stored insessionStorage.wizardMyListsAutoto tell Lists.vue to auto-process files.autoProcessInitialFileflag is stored insessionStorage.autoProcessInitialFile.
Every stage transition and modal open/close is logged to the setup log
file via POST /api/wizard-log. The logWizardEvent() function in
Lists.vue and addSetupLogLine() in ChatInterface.vue handle this.
Progress entries are written every ~60 seconds during polling.
Tab opens are emitted by MyStuffDialog via 'tab-opened' events and
logged by ChatInterface's handleMyStuffTabOpened(). Brief Saved Files
tab opens (< 1 second) are suppressed.
Sign-out behavior depends on the account type.
Modal: showTempSignOutDialog
- "You're signed into a temporary account."
- CREATE A PASSKEY: Opens passkey registration (converts temporary to persistent).
- DESTROY ACCOUNT: Opens the Destroy dialog (see Section 5).
- SIGN OUT: Calls
handleTemporarySignOut()— signs out without destroying. The temp session cookie persists; user can resume later.
- If the user has shared deep links: shows the Dormant Dialog.
- If no deep links and no local backup: offers Passkey Backup first.
- Otherwise: proceeds directly to dormant sign-out.
Modal: showDormantDialog
- "Deep links require a running server."
- KEEP SERVER LIVE: Signs out locally but server stays active for deep-link recipients. Saves local snapshot.
- GO DORMANT: Saves local snapshot, calls
POST /api/account/dormantto pause the server, then signs out.
Modal: showPasskeyBackupPromptModal
- "Encrypt a backup with a 4-digit PIN?"
- NO: Skips backup, proceeds to sign-out. Sets a flag so the prompt doesn't repeat.
- YES: Opens PIN dialog.
Modal: showPasskeyBackupPinDialog
- User enters a 4-digit PIN.
- Snapshot is encrypted with the PIN and saved to localStorage.
- Then proceeds to sign-out.
During sign-out, saveLocalSnapshot() writes the user's current state
to their local folder as maia-state.json (files list, medications,
patient summary, saved chats, agent instructions). This enables restore.
Temporary users can reach Destroy via the sign-out dialog's "DESTROY ACCOUNT" button.
Modal: showDestroyDialog (persistent)
Verification step: user must type their exact userId to confirm.
- Displays: "This permanently deletes your cloud data for {userId}. Signing out is reversible; destroying is not."
- Input field: "Enter user ID"
- DESTROY button: enabled only when typed text matches
user.userIdexactly.
destroyTemporaryAccount():
- Saves a local state snapshot (for potential restore)
- Adds log entries: "Cloud account deleted by user" (bold) and "Local backup preserved in folder for restore"
- Regenerates
maia-log.pdfwith the deletion entries - Calls
POST /api/self/delete, which runsdeleteUserAndResources(userId):- Deletes Spaces files under
{userId}/ - Deletes Knowledge Base by stored
kbId - Deletes Agent by
assignedAgentId+ scans for orphan agents - Deletes session documents
- Deletes the user document from CouchDB
- Deletes Spaces files under
- Clears IndexedDB snapshot with
keepDirectoryHandle: true— this preserves the folder handle sodiscoverUsers()can find the user and show an orange-bordered card with RESTORE button - Resets auth state (back to Welcome page)
When a destroyed user's card shows on the Welcome page with status "restore" (orange), the user clicks RESTORE.
- Read local state: Tries in order:
- Stored folder handle → reads
maia-state.json - IndexedDB saved handle → reads
maia-state.json - Prompts user to pick their local folder
- Falls back to IndexedDB snapshot
- Stored folder handle → reads
- Recreate user doc:
POST /api/account/recreate→ creates fresh CouchDB user doc withkbNamepre-set (from snapshot or generated). - Cloud health check:
GET /api/cloud-health→ verifies what exists in DigitalOcean. - Launch RestoreWizard: Opens with local state and cloud health.
Modal: RestoreWizard component (persistent dialog with X close button)
Runs automatically on mount — no user interaction required. The user can close the dialog mid-restore via the X button; the restore continues in the background.
Emits restore-log events at each step, which App.vue forwards to
ChatInterface's addSetupLogLine(). The log starts with a bold
"Restore started" entry listing file count, medications, and summary
availability.
- Step 1: Upload files from local state →
POST /api/files/uploadand register metadata (each file logged individually) - Step 2 (parallel): Deploy agent →
POST /api/sync-agent?create=true - Step 3 (parallel): Index KB →
POST /api/update-knowledge-base - Steps 4-7: Restore medications, patient summary, saved chats,
and agent instructions from the local state snapshot via
POST /api/restore(each item logged) - Step 8: Restore My Lists markdown via
POST /api/files/lists/restore-markdown
After RestoreWizard completes, a bold "Restore complete" log entry is
added with a summary, and maia-log.pdf is regenerated.
After the RestoreWizard completes:
- Folder identity is re-stamped with the current userId
- A personalized
.weblocshortcut is written - Local state snapshot is updated
- Agent status is checked to confirm endpoint is ready
- ChatInterface's
loadProviders()is triggered (viarestoreActivewatcher) so the AI dropdown switches from "Anthropic" to "Private AI" maia-log.pdfis regenerated with all restore log entries
Accessed via the MORE CHOICES button on the Welcome page.
Modal: showOtherAccountOptionsDialog
Available actions depend on user type:
- Sign in as a different user: Opens passkey auth.
- Delete Cloud Account for {userId}: Requires passkey verification first.
- Delete Local Storage for {userId}: Clears localStorage snapshot.
Modal: showMoreChoicesConfirmDialog (kind = 'delete-cloud')
For cloud users (with passkey):
- Keep local backup and delete cloud: Saves snapshot locally, then
calls
POST /api/account/dormant. - Delete everything: Calls
POST /api/self/deleteand clears local snapshot.
For local-only users:
- Single DELETE button: restores temp session, calls
POST /api/self/delete, clears snapshot, signs out.
Modal: showMoreChoicesConfirmDialog (kind = 'delete-local')
- Single DELETE button: clears
userSnapshotfor the userId from localStorage, reloads welcome status.
MAIA supports multiple family members using the same device.
User discovery has been refactored from a localStorage-based
knownUsers[] array to a dynamic discoverUsers() function that
scans IndexedDB for stored FileSystemDirectoryHandle entries.
Each discovered user is represented by a DiscoveredUser interface:
userId— unique identifier (e.g. "chloe73")displayName— patient name extracted from.weblocor state filefolderName— local folder namehandle—FileSystemDirectoryHandle(if permission granted)cloudStatus—'ready'|'loading'|'restore'
The Welcome page displays a card for each discovered user with their current cloud status, determined by checking server-side existence.
- Each user has their own CouchDB document, DO agent, KB, and Spaces folder (all prefixed by userId).
- Session cookies are per-user.
- Snapshots are keyed by userId in localStorage.
- Folder handles in IndexedDB are per-user.
maia-state.jsonin each user's folder stores their state.
- Destroy: Requires typing the exact userId to confirm.
- Passkey: Each user registers their own passkey, tied to their userId.
- Shared device mode: Forces passkey registration immediately after account creation, preventing unauthorized access.
- User cards: Color-coded (green/grey/orange) to make each user's status immediately obvious.
- Disambiguation dialog: When restorable users exist and someone clicks GET STARTED, a dialog prevents accidentally creating a new account instead of restoring.
Each user card has an X button that calls handleDeleteLocalUser():
- Removes the user from
knownUsers - Cleans MAIA files from their local folder (if handle available)
- Clears their IndexedDB snapshot
- Optionally deletes their cloud account if still active
MAIA uses semantic versioning with these rules:
| Segment | When it changes |
|---|---|
| Major (X.0.0) | Incompatible database or backup/restore format changes |
| Minor (0.X.0) | Major new functionality added |
| Patch (0.0.X) | Each app update (bug fixes, UI tweaks, minor improvements) |
The version is stored in package.json and can be displayed in the
app's About section. A major version bump signals that existing backups
or CouchDB documents may not be compatible and migration steps are
needed.
The following issues were identified and fixed:
-
Patient Summary "I'm sorry" blocking wizard flow: When the AI returned a refusal like "I'm sorry..." as the summary (due to KB not yet indexed), the medications→summary transition would display this text and stop, requiring manual user action. Fixed by always triggering
'generate-summary'or'update-summary-meds'when transitioning from medications to summary phase, instead of skipping the action when medications didn't change. -
Verify dialog interrupting wizard: During the wizard medications phase, the verify prompt dialog appeared immediately, removing the red EDIT/VERIFY button borders. Fixed by suppressing the dialog during
wizardAutoFlowand keepingneedsVerifyActiontrue when dismissed. -
Edit mode flash during wizard: Medications briefly showed in edit mode (blank textarea) before data loaded. Fixed by deferring to file processing during wizard flow instead of opening manual entry.
-
Patient Summary tab flash: Stale or loading content briefly appeared when switching to the summary tab. Fixed by always setting
loadingSummary = trueon tab switch. -
Orange badge missing after DELETE CLOUD ACCOUNT: The
clearUserSnapshotfunction was removing the directory handle from IndexedDB, making the user invisible todiscoverUsers(). Fixed by addingkeepDirectoryHandle: trueoption. -
RESTORE button missing from orange badge: Dropped during the Welcome page refactor from
knownUserstoDiscoveredUser. Fixed by re-adding a conditional RESTORE button. -
ku.userIdreference error: Lines inhandleUserCardRestorereferenced old parameter namekuinstead ofduafter refactor. Fixed by updating todu.userId. -
maia-log not updated during DELETE CLOUD ACCOUNT: No logging mechanism existed for App.vue to write to the setup log. Fixed by exposing
addSetupLogLineandgenerateSetupLogPdfviadefineExposeon ChatInterface. -
RestoreWizard had no close button: Unlike the Setup Wizard, the restore dialog was persistent with no X button. Fixed by adding an X close button that allows the restore to continue in the background.
-
RestoreWizard was a logging black hole: Zero
addSetupLogLinecalls throughout the entire restore process. Fixed by addingrestore-logemit events at each step, forwarded through App.vue to ChatInterface's setup log. -
Provider selection not updating after restore: The AI dropdown showed "Anthropic" instead of "Private AI" after restore because
loadProviders()wasn't triggered. Fixed by adding a watcher onrestoreActivethat callsloadProviders()when restore completes. -
Lists Source File footer shown during wizard: The "LISTS SOURCE FILE" section appeared during the guided flow. Fixed — template already has
&& !wizardAutoFlowguard. -
Dialog-to-dialog transition flash: The wizard closing before My Stuff opened could cause a single-frame flash. Fixed — code now opens My Stuff before closing the wizard (same tick, Vue batches).
-
Setup wizard logging gaps: Several transitions were not logged: initial 'running' phase entry, reload-triggered phase resumption, and first guided-flow dismissals. Fixed by adding
addSetupLogLinecalls at all six locations (Chrome/Safari running entry, two reload resume paths, two first-dismiss paths).
The Lists component inside MyStuffDialog's tab panel is recreated every
time the user switches away and back. This means onMounted re-runs,
triggering loadCurrentMedications(), checkInitialFile(), and
potentially attemptAutoProcessInitialFile() again. During the wizard
flow, this can cause redundant API calls and brief UI flashes.
Suggestion: Either wrap Lists in <KeepAlive> so it preserves
state across tab switches, or add guards in onMounted to detect that
the component was previously initialized for this session.
Lists.vue has two onActivated() hooks at different locations (lines
2179 and 2535). Both fire independently when the component is
re-activated. The first handles wizard/verify state; the second handles
category reloading and auto-processing. This separation makes the
activation flow harder to reason about and increases the risk of
competing triggers.
Suggestion: Merge into a single onActivated hook with clear
sequential logic.
Setup Wizard: wizardFlowPhase resets to 'done' on every page
reload. A resume watcher tries to detect mid-flow state by checking
server-side flags. This mostly works but:
- If the user reloads during the 'running' phase (indexing in progress), the wizard dialog disappears. When indexing later completes, the wizard dialog suddenly reappears — potentially confusing.
Restore Wizard: If the user reloads during a restore, the wizard state is completely lost. The restore processes continue on the server but the UI has no way to resume or show progress. The user sees a normal chat interface with no indication that restore is still running.
Suggestion: For both wizards:
- Persist wizard state to sessionStorage (
wizardFlowPhase,restoreActive, current step). - On reload, detect active wizard state and show an appropriate "Resuming..." indicator.
- For RestoreWizard, poll server-side status to determine what has already completed and resume the checklist accordingly.
- Add a
workflowStage: 'restoring'value to CouchDB that blocks the setup wizard from launching during an active restore.
During the guided flow, the user can freely click other tabs (Saved Files, Privacy, Diary, etc.), breaking the expected flow. There are no warnings, no prevention, and no easy way back.
Suggestion: Either disable non-relevant tabs during the guided flow, or show a "Return to {current step}" banner when the user navigates away.