EBsoft HMSBuild & Architecture Guide

Build Guide / 05

Database & security

There's no traditional SQL schema here — the database is Cloud Firestore, and the schema effectively lives in firestore.rules, which is both the access-control layer and the closest thing to a data contract.

Firestore as the system of record

Collections are read and written directly from the React client using the Firebase v11 SDK (src/lib/firebase.ts), with real-time listeners (onSnapshot) used anywhere the UI needs to reflect changes made elsewhere — a bed becoming free, a new lab result, a notification. Writes that must bypass per-user rules (like the daily bed-charge cron) go through firebase-admin on the server instead.

How access control is modeled

firestore.rules defines a small set of reusable predicates that every collection's rules are built from:

function isSignedIn() {
  return request.auth != null;
}

function isEmailVerified() {
  return isSignedIn() && request.auth.token.email_verified == true;
}

function getUserData() {
  return (isSignedIn() && exists(/databases/$(database)/documents/users/$(request.auth.uid)))
    ? get(/databases/$(database)/documents/users/$(request.auth.uid)).data
    : null;
}

function hasRole(roleName) {
  return isSignedIn() && exists(/databases/$(database)/documents/users/$(request.auth.uid)) && ...
}

Every collection's rule is then written in terms of these — "signed in and has role X" or "signed in and is the owner of this document" — rather than repeating auth logic per collection. The rules file opens with an explicit default-deny for anything unmatched, so a new collection is inaccessible until a rule is written for it.

Storage

storage.rules governs Firebase Storage the same way, for anything uploaded rather than written as structured data — scanned documents, DICOM images, profile photos.

Testing the rules

test/firestore.rules.test.ts uses @firebase/rules-unit-testing to exercise the rules directly against the Firestore emulator — asserting that a nurse can't read accounts data, a patient can't read another patient's record, and so on — independent of the application code.

Defense in depth on the client

Firestore rules are the actual security boundary, but the app doesn't stop there: dompurify sanitizes any HTML rendered from stored strings, and the audit logger in src/lib/auditLogger.ts records who changed what, which matters as much for accountability in a hospital setting as it does for debugging.

Firestore security rules are the single most important file to get right in this stack — a mistake there is a data leak, not a bug report. Any change to firestore.rules should be run against the rules test suite before it ships.