- Primary remote:
origin=HIEofOne/self(all PRs and pushes go here) - Upstream fork:
upstream=agropper/self(do NOT push or create PRs here without explicit permission) - When creating PRs, always use
--repo HIEofOne/self
- Feature branches:
claude/<short-description> - PRs target
main
- A pre-commit hook (
scripts/pre-commit-version-bump.sh) auto-increments the patch version inpackage.jsonwhenserver/orsrc/files are staged - Do not manually bump versions unless doing a minor or major release
- Frontend: Vue 3 (SFC, Composition API), Vite, TypeScript for utilities
- Backend: Express.js (ESM), CouchDB/Cloudant, DigitalOcean GenAI agents + Spaces (S3)
- Auth: Passkey/WebAuthn with temporary (no-passkey) user fallback
- Local storage: File System Access API (primary, Chrome 122+), PouchDB/IndexedDB (fallback)
Line counts as of v1.6.4 (September 2026); whole repo ≈ 71,000 lines (src/ 39,000 + server/ 31,600 + scripts/ 645), plus tests/ 3,400.
server/index.js— main backend (~15,200 lines)src/components/ChatInterface.vue— wizard, chat UI (~10,500 lines)src/components/MyStuffDialog.vue— Workbook: saved files, patient summary, lists, privacy filter (~8,300 lines)src/App.vue— main frontend entry, auth, welcome page (~4,300 lines)src/components/Lists.vue— deterministic lists, meds verification (~3,400 lines)server/routes/groups.js— group registry, membership, relay, outside requests (~4,700 lines). Member messages are sealed in the browser (src/utils/memberMessages.ts) to the recipient's folder key; hosts store them sealedserver/routes/files.js— file upload, PDF parsing (~3,100 lines)server/routes/auth.js— passkey auth, agent provisioning (~1,700 lines)server/routes/chat.js— chat providers, deep link resolution (~1,000 lines)server/credits.js— credits ledger, Stripe webhook (~440 lines)server/routes/policies.js— sharing-policy cards, vocabulary, evaluator (~300 lines)server/records-pipeline.js— derived view of a user's records journey (~180 lines)server/ask-maia.js+server/routes/ask-maia.js+server/pr-history.js+src/components/AskMaia.vue/AskPage.vue— "Ask Claude about MAIA" (/ask, a card on the welcome page,WelcomeMaps.vue): Claude (MAIA_ASK_MODEL) gets a cached knowledge pack (Documentation/Ask_MAIA_Brief.md, a code map from each file's header comment, a documentation map, the merged-PR history from GitHub) and reads what it needs with read-only tools (read_file, read_pr, search, grep_repo). Keep the brief current, and start each file with a one-sentence header comment (the code map shows it)src/utils/passkeyFolderKey.ts+src/components/PhoneAccess.vue— companion mode (a phone or Safari, no folder): the folder key travels with the passkey (WebAuthn PRF; the server keeps only an encrypted copy,/api/folder-key/wrapped); passkey-only sign-in is/api/passkey/discoverserver/utils/records-text.js+clinical-prompts.md(patient-summary.records-in-context) — the Patient Summary drafted from the records' full text (page-marked, File N order); the default private AI is Qwen3.8-Max (server/utils/primary-models.js,MAIA_PRIMARY_MODEL)server/forum.js— the Community Forum (Discourse): Ask questions posted as topics, and the forum's webhook emailing the maintainer (DISCOURSE_*,FORUM_NOTIFY_EMAIL)server/public-ais.js— the public AIs in the chat's AI menu (Personal AS): one chat provider per DO catalog model, probed at startup; listed but locked until the patient turns onpublic-ai(the chat route refuses until then). Images attached in chat stay in the browser and go only to models that read imagesserver/edition.js— edition switch (MAIA_EDITION) and feature registry (~190 lines)server/edition-routes.js— every route's feature, and the /api feature gate. Adding a route? Add it toROUTE_FEATURES, ortests/backend/edition-routes.test.jsfailsserver/utils/api-guard.js— /api account-access guard: the session decides the account; in the Personal AS edition the admin acts for others only on/api/admin/and/api/billing/(~140 lines)server/routes/gnap.js+server/gnap/— GNAP personal AS (direct route): RFC 9421 signatures, grants, RS; reference clientscripts/gnap-client.mjsserver/advisor-context.js— what the private AI is told: the full edition's Policy Advisor, and the Personal AS edition's context on every private-AI turn (summary, rules, requests, features to turn on, and help with MAIA itself: their MAIA's state, their recent maia-log, MAIA in brief, and user-facing documentation for a how-to question). Proposals come back as fencedpolicy-card/maia-featureblocks (src/utils/advisorProposals.ts); only the patient's click saves or turns anything onserver/routes/gnap-out.js— this MAIA asking another MAIA for its user (P11): pairwise keys, the email hand-off (/gnap/client/finish/:id), server-side waiting, answers sealed to the folder key →Received/; drafts come from the private AI as fencedmaia-requestblocks, sent only on the user's clickserver/gnap/documents.js+server/routes/received.js— documents others add (P9): descriptor and type checks, holds sealed to the patient's folder key, limits; the folder key and the holds for the patient's browser (src/utils/folderKey.ts,src/utils/received.tswrite them toReceived/)server/routes/gnap-group.js+server/gnap/group.js— GNAP group routing: the group's endpoint, fan-out of signed copies with a group attestation, sealed answers (the member's side isreceiveGroupCopyinroutes/gnap.js)src/gnap/+src/components/RequestPage.vue/GroupRequestPage.vue— the browser GNAP client and the requester's pages at/r/<asId>and/g/<groupId>/request(mounted bymain.ts);RequestsPanel.vueis the patient's sideDocumentation/Claude_Notes.md— Claude's working notes (conventions, invariants, local test recipes), public; no secrets or vulnerability details thereDocumentation/group_requests.md— design of the Personal AS edition (phases P0–P12);public/MAIA_Request_Map.html(who runs what, how requests travel) andpublic/MAIA_Group_Network.html(how hosts, groups and people connect), served at those paths and linked from the welcome page — update them when a request path changes
The goal is to minimize secrets. Several values are derived from the DO token at startup so they don't need separate env vars.
| Variable | Purpose |
|---|---|
DIGITALOCEAN_TOKEN |
Master secret. Used for DO API calls, to derive CouchDB password, session secret, admin passphrase, create DO Inference Model Access Key, and auto-discover/create the OpenSearch cluster. |
SPACES_AWS_ACCESS_KEY_ID |
S3-compatible access key for DO Spaces (cannot be derived from DO token — Spaces uses the separate S3 API). |
SPACES_AWS_SECRET_ACCESS_KEY |
S3-compatible secret key for DO Spaces. |
PUBLIC_APP_URL |
The public URL of the app (e.g. https://test.agropper.xyz). Controls secure cookies, trust proxy, and passkey origin. |
Optional: MAIA_EDITION = full (default) or personal-as, read once at startup (see Documentation/Environment.md). MAIA_HOST_ROLE=group-only makes an app a group-only host (no patient accounts, no private AIs, no health records; welcome page = GroupHostWelcome.vue), as for trustee.ai (Documentation/Trustee_Host.md).
Chat provider keys (ANTHROPIC_API_KEY, CHATGPT_API_KEY, DEEPSEEK_API_KEY) are not needed in production. All three providers are routed through DO Serverless Inference automatically. GEMINI_API_KEY is optional (Gemini is not available on DO Inference).
| Value | Derivation |
|---|---|
| CouchDB password | HMAC-SHA256(token, 'maia-couchdb-admin') base64url, first 32 chars |
| Session secret | HMAC-SHA256(token, 'maia-session-secret') base64url, first 32 chars |
| Admin passphrase | DO token used directly (pasted once at first admin login, then passkey takes over) |
| Admin username | Hard-coded to admin |
| CouchDB username | Hard-coded to admin |
| DO Inference key | Created via DO API, cached in CouchDB maia_config/do_inference_key |
| OpenSearch database ID | Discovered via DO API (GET /v2/databases?engine=opensearch), or created if none exists. Cached in CouchDB maia_config/opensearch_database_id. One cluster per account enforced. |
| Port | Defaults to 3001; DO App Platform sets PORT automatically |
| Variable | Typical value | Purpose |
|---|---|---|
PUBLIC_APP_URL |
http://localhost:5173 |
Local Vite dev server |
CLOUDANT_URL |
http://localhost:5984 |
Local Docker CouchDB |
DIGITALOCEAN_TOKEN |
dop_v1_... |
Required for DO API calls |
SPACES_AWS_ACCESS_KEY_ID |
DO00... |
Same as production |
SPACES_AWS_SECRET_ACCESS_KEY |
f1Ru... |
Same as production |
For detailed environment documentation, see Documentation/Environment.md.
If the DO token is rotated, the derived CouchDB password changes but the CouchDB droplet still has the old one. Update the CouchDB admin password via PUT /_node/_local/_config/admins/admin or SSH to the droplet. Delete maia_config/do_inference_key and maia_config/opensearch_database_id in CouchDB so they are re-created on next startup. Session secret rotation logs out all users (they re-authenticate with passkeys).
npm testruns every vitest suite;npm run test:backendrunstests/backend/. Most suites use in-memory fakes.auth,cloudantandhealthneed a local CouchDB (Docker,admin/adminpassatlocalhost:5984).- Edition work: new backend tests run under both editions (
describe.each(EDITIONS)withsetEditionForTests), andfullmust behave exactly as before. - supertest: pass
await serve(app)(tests/helpers/serve.js), not the bare app.request(app)listens on[::]but connects to127.0.0.1, and on macOS another program's127.0.0.1listener can answer instead (intermittent 404s). - UI flows still need manual testing in the running app.
- Build check:
npm run build— the DO deploy runsvue-tsc && vite build, andvue-tscfails on things vite tolerates (e.g. TS6133 unused declarations).npx vite buildalone is NOT sufficient.