Compliance & Security Architecture
This page describes the compliance architecture as implemented in the DPerspective backend: how Protected Health Information (PHI) is encrypted at rest and in transit, how keys are managed, how the audit trail is made tamper-evident, and how GDPR/HIPAA controls are enforced.
This page documents only controls that are implemented in the codebase. Where something is deliberately excluded, it is called out explicitly under Out of scope.
Encryption at rest
PHI is protected with application-level field encryption rather than
whole-database encryption. Encryption is interposed inside the datastore facade
(getDb() in backend/src/config/db.js), so every controller that already
writes through the facade gets encryption transparently — no controller changes
are required.
-
Cipher —
AES-256-GCM, an authenticated mode. The GCM authentication tag provides per-field tamper detection for free. -
Where it happens — on write, the facade replaces each registered PHI field value with an Encrypted_Envelope before the document is persisted as a JSON blob. On read, envelope-valued fields are decrypted back to plaintext before the document is returned to the caller. A written-then-read document is deep-equal to the original (round-trip).
-
Encrypted_Envelope format — each encrypted value is stored as a small object carrying the ciphertext and everything needed to decrypt it:
{ "__enc": 1, "v": 1, "kid": "k1", "iv": "base64(12 bytes)", "tag": "base64(16 bytes)", "ct": "base64(ciphertext of JSON.stringify(value))" }The
__encsentinel marks the value as an envelope; any value lacking it is treated as plaintext, which keeps pre-existing data readable and lets it be encrypted on its next write. -
PHI field registry — a per-collection registry (
phiRegistry.js) declares exactly which fields are PHI (for exampleconditions→name,severity,sinceDate,notes). Only registered fields are enveloped; everything else is left untouched. -
Indexed identifiers stay plaintext —
userId,tokenHash, andclientIdback the indexed VIRTUAL generated columns used for SQL equality lookups, so they are deliberately never encrypted and remain queryable. Existing credential hashes (SHA-256 token/secret/code hashes, bcrypt password hashes) are likewise left byte-identical.
Even with full datastore disclosure, PHI fields are stored only as AES-256-GCM ciphertext, and credentials remain only as one-way hashes.
Encryption in transit
nginx terminates TLS in front of the backend:
- Accepts
TLS 1.2andTLS 1.3and refuses any lower protocol version. - Redirects plaintext HTTP to its HTTPS equivalent with a
301. - Presents a valid Let’s Encrypt certificate for
dperspective.galacticgeeks.com. - Sends an HTTP Strict-Transport-Security (HSTS) header on HTTPS responses. An application-level HSTS middleware sets the same header as defense in depth.
The nginx TLS configuration is kept as a version-controlled artifact in the
repository (deploy/nginx-dperspective.conf).
Key management
- Master_Key — a single 256-bit key loaded at startup from the
PHI_MASTER_KEYenvironment variable (base64). No external KMS or HSM is used on the self-hosted single node. - Derived Data_Keys — per-
keyIdData_Keys are derived deterministically from the Master_Key viaHKDF-SHA-256. Because derivation is deterministic, Data_Keys never persist — only the Master_Key needs protecting. - Key metadata registry — the set of known
keyIds and theirstatus/createdAt/retiredAtlives in a non-secretkeyMetadatacollection that holds no key material. - Rotation — introducing a new key adds a new active
keyIdfor new encryptions while retaining priorkeyIds so previously stored PHI stays decryptable. - Fail-closed — if
PHI_MASTER_KEYis missing or shorter than 256 bits at startup, the key manager refuses all PHI encryption and decryption (including any cached or in-memory PHI) and reports a configuration error. When an envelope references an unresolvablekeyId, decryption returns an error immediately — no fallback, no partial or placeholder PHI.
Operational risk: on a single self-hosted node with no KMS, loss of the Master_Key is unrecoverable — all PHI at rest becomes permanently undecryptable. The Master_Key is backed up out of band and is never written to logs, the audit trail, or any datastore backup.
Audit trail & tamper-evidence
The append-only audit log records security-relevant events and is made tamper-evident with a hash chain:
- Each entry carries a monotonic
seq, the previous entry’s hash (prevHash), and its ownentryHashcomputed over its canonical content plusprevHash. - The log is append-only — no operation updates or deletes an existing entry. Appends are serialized inside a synchronous transaction so entries are totally ordered (a documented single-node assumption).
verifyChain()walks the chain oldest-first and reports whether it is intact, pinpointing the first entry where verification breaks if it is not.- Entries are sanitized to documented fields only, so no plaintext secrets, tokens, or PHI ever enter the audit log.
Data integrity validation
- On write, an integrity hash covering a PHI document’s content is recorded in a
phiIntegritycollection. - Recomputing and comparing that hash detects out-of-band modification at the document level; a mismatch flags the document as failing and appends a PHI-free failure entry to the audit log.
- At the field level, the AES-256-GCM authentication tag detects tampering on decrypt — a failed tag check surfaces as a decryption error rather than returning corrupted data.
- A batch operation validates all stored PHI documents and reports the counts validated and failed.
Secure backup & restore
- Backups snapshot the SQLite datastore. Because PHI is already enveloped at rest, backups inherit that encryption and contain no plaintext PHI.
- The Master_Key is never included in a backup.
- Restore verifies the backup’s integrity hash first and aborts, leaving the existing datastore unchanged, if the backup is corrupted.
- Both backup and restore append an audit entry recording the operation and its outcome.
FHIR export
A per-user export produces FHIR R4 resources at:
GET /compliance/export/fhirThe route is guarded by the existing authentication middleware so the export is
scoped to the requesting user (req.user.id) and contains only that user’s PHI.
Values decrypt transparently as they are read through the facade. Internal
collections map to FHIR resources (for example healthData → Observation,
conditions → Condition, medications → MedicationStatement,
allergies → AllergyIntolerance, users/profile → Patient). Categories
with no FHIR mapping are excluded from the bundle and reported in the export
result. Each export appends an audit entry recording the requesting identity and
the subject userId.
Legal documents
The platform serves versioned EULA, Privacy Policy, and Terms & Conditions documents, each with a version identifier. The Privacy Policy describes the PHI categories collected, the encryption safeguards applied, and the data-subject rights of access and erasure.
When a user accepts an available document, a Consent_Record is stored capturing the user identity, document type, accepted version, and a UTC ISO 8601 timestamp. Accepting a version that is not currently available is rejected and creates no record, and Consent_Records for prior versions are retained when a new version is published.
GDPR & HIPAA controls
- Right to access — a data subject can retrieve all PHI held for them in a readable form.
- Right to erasure — a data subject can delete their PHI documents; the erasure appends an audit entry, and PHI-free audit accountability entries are retained so the record of the erasure itself is preserved.
- Per-user isolation — every PHI read is scoped by
userId, so an access request for one subject never returns another subject’s PHI. - Data minimization — collected PHI fields are limited to those defined for the stated purpose.
Out of scope
Multi-cloud storage and cloud region selection are out of scope.
PHI persists exclusively in the self-hosted local SQLite datastore. The following are deliberately excluded and have no code path in this implementation:
- Multi-cloud provider abstraction or backend setup for AWS, GCP, or Azure.
- User-facing cloud provider selection and region picker.
- Any cloud-provider migration path or multi-cloud synchronization of PHI.