ARCHITECTURE_ESSENTIALS.md — docforum-web
Frontend-only quick reference. Full product/system architecture lives in docforum-core's ARCHITECTURE.md and ARCHITECTURE_ESSENTIALS.md — read those first if you're new to the org, not just this file.
Stack
- React + TypeScript + Vite
- React Query for server state, Zustand for client-only UI state
- No localStorage/sessionStorage for anything server-derived — treat
docforum-core's API as the source of truth, always refetch/cache via React Query rather than hand-rolling persistence.
Hard rules
- This repo never calls
docforum-escrowor Stellar directly. Wallet linking, payment status, escrow state — all of it comes fromdocforum-core's API. If a screen needs Stellar-related data, that's adocforum-coreAPI gap to file, not a reason to add a Stellar SDK dependency here. - Mirror
docforum-core's data model shapes insrc/types/models.ts— don't invent parallel frontend-only shapes for the same entities. - Structure:
pages/ components/ features/ hooks/ services/ store/ types/— keep this shape.
Where things live
src/services/— API client calls todocforum-core.api-client.tsis the only place fetch/base-URL/error-envelope details live.src/features/— flow-specific logic (booking, referral-accept, order fulfillment selection, etc.) — one feature folder per PRD core flow.src/pages/— route-level components.src/store/— Zustand stores.auth-store.tsis intentionally in-memory only (not persisted) — see hard rule about localStorage/sessionStorage above; it applies to auth tokens too, not just business data.
Deployment
- GitHub Pages, deployed on every push to
mainvia.github/workflows/deploy-pages.yml: https://docforum.github.io/docforum-web/ - Connected to a live backend as of 2026-09-14 —
VITE_API_BASE_URLis set at build time todocforum-core's Render preview deployment (https://docforum-core-api.onrender.com, see that repo'sdocs/adr/0003-render-preview-deployment.md). Signup/login actually work end-to-end against a real Postgres now. Still a preview, not production: the free-tier backend spins down after 15 minutes idle (~1 minute cold-start delay on the next request), and its Postgres expires 2026-10-14 unless renewed — data is disposable. - Router is
HashRouter, notBrowserRouter— GitHub Pages has no server-side rewrite for client-side routes, so a deep link like/loginwould 404 on refresh under history-API routing. Switch back toBrowserRouterif this ever deploys somewhere with a real rewrite rule (e.g. behinddocforum-coreitself, or Vercel/Netlify). vite.config.ts'sbaseis conditional onGITHUB_PAGES=true(set bynpm run build:pages) — plainnpm run buildstill serves from/.
Known gaps against the real API (docforum-core Phase 1 landed 2026-09-14)
docforum-core now has a real, tested API — see its docs/api/README.md. The original W1 assumptions here turned out correct (POST /auth/signup/ login/logout, { user, accessToken }, { message: string } error envelope, fullName is a real column, role restricted to patient/ doctor at signup — server-enforced too, not just this UI). credentials: 'include' is now set on the fetch call in api-client.ts (fixed 2026-09-14, alongside the Render connection — the real backend's refresh cookie is SameSite=None; Secure, cross-site between this repo's github.io domain and Render's, so it's required for the cookie to be sent/received at all). One gap remains:
POST /auth/refreshexists on the backend but nothing here calls it.auth-store.tsstill has no silent-reauth-on-reload flow — a page refresh loses the session even though the refresh cookie would still be valid. Tracked, not built (Phase W1 was scoped to signup/login only).- Local-dev-only caveat inherited from the backend: its refresh cookie is
sameSite: 'lax'/secure: false, which may not survive true cross-origin (different ports)fetch+credentials: 'include'in every browser — not verified end-to-end. Seedocforum-core'sdocs/api/README.md.
Known assumptions — Phase W2 (dependents, health profile, acting-for)
These API shapes are assumed and not yet implemented in docforum-core. Reconcile against the real contract once the corresponding backend issues land.
Dependents (docforum-core#42)
GET /patients/me/dependents→Dependent[]POST /patients/me/dependents(body:{fullName, dateOfBirth, relationship}) →DependentPATCH /patients/me/dependents/:id(body:{fullName, dateOfBirth, relationship}) →DependentPOST /patients/me/dependents/:id/handover-invite(body:{email}) →DependentPOST /patients/me/dependents/:id/handover-invite/resend→DependentPOST /auth/claim-account(body:{token, email, password}) →voidDependentshape:{id, guardianProfileId, fullName, dateOfBirth, relationship: 'parent'|'guardian'|'carer', handoverStatus: 'not_eligible'|'eligible'|'invited'|'claimed', claimEmail?, claimInviteExpiresAt?, createdAt, updatedAt}
Health profile (docforum-core#59)
GET /patients/:profileId/health/:section→HealthEntry[]POST /patients/:profileId/health(body:{section, name, details?}) →HealthEntryPATCH /patients/:profileId/health/:entryId(body:{name, details?}) →HealthEntryDELETE /patients/:profileId/health/:entryId→voidHealthEntryshape:{id, patientProfileId, section: 'allergies'|'conditions'|'medications', name, details?, version, previousValue?, updatedAt, createdAt}
Acting-for profile switcher (#20)
No new backend endpoint. The switcher is client-only UI state (Zustand store). Every patient API call and React Query key includes the selected patientProfileId so caches never mix profiles.
Data export (docforum-core#40, issue #26)
POST /account/data-export(body:{password}) →DataExportGET /account/data-export→DataExport | nullGET /account/data-export/:id/download→ file stream (application/json)DataExportshape:{id, patientProfileId, status: 'pending'|'processing'|'ready'|'expired'|'failed', requestedAt, readyAt?, expiresAt?, fileSizeBytes?, downloadUrl?, errorMessage?}- Ready exports expire after 24 hours. Downloads require bearer authorization and avoid keeping export contents in memory.
Account deletion (docforum-core#41, issue #27)
POST /account/delete(body:{password, confirmation: 'DELETE'}) →{success, message, deletedAt}- Clinical and statutory retention: identity credentials (name, email, password hashes, guardian links) are permanently scrubbed/anonymized. Clinical consultation notes, diagnostic orders, and cryptographic payment/escrow records are retained by law in de-identified form.
- Deletion is blocked if there are active orders or unreleased funded escrows.
- Client clears all QueryClient cache and Zustand session/profile state.
Care thread timeline (issue #28)
GET /patients/:patientProfileId/care-threads→CareThread[]GET /care-threads/:id→CareThread- Aggregates appointments, consultations, referrals, orders, fulfillment, and escrow payment settlement into a single continuous journey timeline.
- Progressive rendering and graceful degradation when modules are not yet implemented.
Facility finder with distance sorting (docforum-core#74, issue #29)
GET /facilities?type=&lat=&lng=&query=→FacilityWithDistance[]- OpenStreetMap tiles with attribution.
- Patient location is requested with transparent consent, kept strictly in transient memory, and NEVER persisted to storage or URLs.
Phase W3 — Doctor core flow (assumed, not yet in docforum-core)
These shapes back issues #45–#49. docforum-core issues #61 (message thread), #129 (in-person check-in / live queue position) and the today's-appointments consultation contracts are not landed yet, so the endpoints below are the smallest reasonable assumption and must be reconciled once the real API exists.
Today's appointments & live queue (issue #49, depends on #6)
GET /doctor/appointments/today→Appointment[]— today's appointments for the signed-in doctor, ordered bystartsAt.GET /doctor/queue/today→QueueEntry[]— checked-in patients in queue order (computed server-side fromcheckInAt).POST /appointments/:id/start→Appointment— marks a checked-in visitin_progress(setsstartedAt). Idempotent if already started.POST /appointments/:id/complete→Appointment— marks an in-progress visitcompleted(setscompletedAt).GET /doctor/queue/running-delay→RunningDelay— computed delay from the current in-progress visit vs its scheduled end, plus any manual override.PATCH /doctor/queue/running-delay(body:{manualOverrideMin?: number | null}) →RunningDelay— set/clear a manual "running late by X min" override.
Undo behaviour: start/complete are applied optimistically and can be reverted within a short undo window (a few seconds) by re-calling the inverse action or clearing the optimistic timestamp; the UI surfaces an undo toast.
Doctor messages inbox (issue #47)
GET /doctor/messages/threads?filter=&cursor=&limit=→{threads: MessageThread[], nextCursor?: string | null}— threads with unread counts, last-message preview (PHI — kept short), waiting time, oldest unanswered first by default.POST /message-threads/:id/read→{}— mark a thread read (clears its unread count).- Header badge reads total unread = sum of
unreadCountacross threads.
Consultation notes sign-off & addenda (issue #45, depends on #7)
GET /consultation-notes/:appointmentId→ConsultationNotePOST /consultation-notes/:id/sign-off→ConsultationNote— setssignedAt/signedBy; note becomes immutable after this.POST /consultation-notes/:id/addenda(body:{content}) →ConsultationAddendum— append-only; no edit/delete UI for addenda.- Auto sign-off deadline, if present, is exposed as
autoSignOffAton the note.
Roadmap / status
See ROADMAP.md in this repo, and Phase 7 in docforum-core's ROADMAP.md for the original (pre-split) frontend phase this was carved out of.