Skip to content

PII Data Encryption & Security Model

ClinicFlow implements defense-in-depth data protection standards to secure Sensitive Personally Identifiable Information (PII), patient health metadata, and third-party API credentials across all persistence and transit layers.


Data within ClinicFlow is classified into three risk tiers:

Tier Sensitivity Data Types Security Controls
Tier 1: High Sensitivity (PII & Secrets) Extreme Patient DUI numbers, tax profiles (NIT/NRC), Meta WhatsApp API access tokens, MH DTE private keys. AES-256-GCM application-layer envelope encryption at rest; restricted DB access.
Tier 2: Protected Health Information (PHI) High Clinical dental entries, periograms, medical notes, appointment reasons, doctor notes. Multi-tenant PostgreSQL Row-Level Security (RLS) isolation; ES256 JWT claim verification.
Tier 3: Operational Data Standard Clinic business names, public doctor schedules, non-PII appointment counts. Standard TLS 1.3 transit encryption; tenant-scoped RLS policies.

2. Application-Layer Envelope Encryption (AES-256-GCM)

Section titled “2. Application-Layer Envelope Encryption (AES-256-GCM)”

High-sensitivity fields (Tier 1) undergo cryptographic envelope encryption before being written to PostgreSQL tables.

  • Cipher: AES-256-GCM (Galois/Counter Mode) providing both confidentiality and authenticated integrity.
  • Master Key: Derived from ENCRYPTION_KEY environment variable (32-byte / 256-bit hexadecimal string).
  • Initialization Vector (IV): Cryptographically secure random 12-byte (96-bit) IV generated per encryption operation via crypto.randomBytes(12).
  • Authentication Tag: 16-byte (128-bit) GCM tag appended to ciphertext to detect tampering.

Encrypted fields are stored in database columns as string blobs formatted with a colon separator:

<iv_hex>:<auth_tag_hex>:<ciphertext_hex>

Encryption Implementation (backend/src/utils/encryption.ts)

Section titled “Encryption Implementation (backend/src/utils/encryption.ts)”
import crypto from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
const MASTER_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
export function encrypt(text: string): string {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(ALGORITHM, MASTER_KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return `${iv.toString('hex')}:${authTag}:${encrypted}`;
}
export function decrypt(encryptedData: string): string {
const [ivHex, authTagHex, ciphertextHex] = encryptedData.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, MASTER_KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertextHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

To mitigate Cross-Site Scripting (XSS) risks, JWT access tokens are stored strictly in frontend application memory (authToken variable in frontend/src/services/api.ts). Access tokens are never saved to localStorage or sessionStorage.

Longer-lived session refresh tokens are delivered in HTTP responses using secure cookies with strict flags:

  • httpOnly: true (Inaccessible to JavaScript)
  • Secure: true (HTTPS transit mandatory)
  • SameSite: Lax (Protection against Cross-Site Request Forgery - CSRF)
  • Path: /api/auth (Restricted cookie delivery path)

  1. TLS 1.3 Encryption: All incoming HTTP traffic to backend and frontend services is forced over HTTPS via Caddy and Railway edge routers.
  2. Database Transit Encryption: Connections between Express Node.js application nodes and Supabase PostgreSQL require SSL (sslmode=require).
  3. Secret Masking & Zero-Logging: Sensitive headers and encrypted values are automatically stripped by Winston logger middleware (backend/src/utils/logger.ts) before writing to log streams.