EBsoft HMSBuild & Architecture Guide

Build Guide / 03

System architecture

Two runtimes, one repository. A client SPA talks to Firestore directly for almost everything, and drops down to a thin Express layer only for the handful of things a browser shouldn't be trusted to do alone.

Request flow

Browser (React SPA)
   │
   ├─ Firestore SDK ──────────────► Cloud Firestore
   │   (reads/writes guarded by      (patients, billing, inventory,
   │    firestore.rules per role)     attendance, settings...)
   │
   ├─ Firebase Auth ──────────────► Google / email+password sign-in
   │
   └─ fetch('/api/...') ──────────► Express app (server-app.ts)
                                        │
                                        ├─ firebase-admin (privileged writes,
                                        │   bypasses client security rules)
                                        ├─ @google/genai (AI assistant)
                                        ├─ node-cron (scheduled bed charges)
                                        └─ resend (email)

Why a server exists at all in a Firestore-first app

Most CRUD in EBsoft HMS goes straight from the React client to Firestore, governed by firestore.rules. The Express layer exists for the narrow set of things that genuinely need a trusted server:

  • Scheduled work — a cron job checks every minute whether it's time to post that day's IPD bed charges, using getAdminFirestore() to write with elevated privileges.
  • AI assistant — the Gemini API key never reaches the browser; requests are proxied through the server.
  • Cross-cutting operations that would be unsafe or unenforceable purely through security rules.

Frontend composition

Inside src/, cross-cutting concerns are lifted into React Context so any of the ~90 page modules can read them without prop drilling:

ContextResponsibility
AuthContextCurrent user, role, sign-in/out
SettingsContextHospital-wide configuration (billing rules, timezone, branding)
NotificationContextStaff-facing alerts and toasts
PatientPortalContext / PatientNotificationContextSession and alerts scoped to the public patient portal

Domain logic that would otherwise be duplicated across pages is pulled into custom hooks — useIPDBilling, usePayroll, useAttendance, useLeave, useRoles, useDepartments, useEmployees — each wrapping the relevant Firestore collection with the reads, writes and derived state a given module needs.

Shared library layer

src/lib/ holds the utilities every module leans on rather than reimplementing: firebase.ts (SDK init + auth helpers), autoNumbering.ts (sequential invoice/admission IDs), auditLogger.ts (who-did-what trail), dateUtils.ts, referralCommission.ts (referral-doctor payout math), sanitizeHtml.ts, imageCompressor.ts, and bangladeshData.ts (districts/upazilas for address fields).

Role-based access

Access control is enforced twice, deliberately: firestore.rules is the source of truth (a client can't bypass it no matter what the UI does), and the React app additionally hides navigation and actions a role shouldn't see, via UserPermissionsWidget and role checks in AuthContext. See Database & Security for how roles are modeled.