Building HIPAA-Compliant SaaS on Modern Stacks
HIPAA compliance sounds like a death sentence for modern development workflows. In reality, it's a set of technical safeguards that map cleanly onto patterns we already use — if you know where to look. This post walks through the architecture decisions we've made across three healthcare SaaS products, all built on modern stacks, all passing SOC 2 and HIPAA audits without turning into fortress codebases.
The Four Pillars of HIPAA Technical Safeguards
The HIPAA Security Rule defines four categories of technical safeguards. Each one maps directly to engineering patterns you likely already use — or should be using. Here's how we implement each in practice.
1. Access Control
Access control is about ensuring only authorized individuals can access electronic protected health information (ePHI). This goes beyond simple login authentication — it's about granular, context-aware authorization that adapts to role, location, and risk level.
- Role-based access control (RBAC) with attribute-based overrides. Users are assigned roles (clinician, admin, billing), but individual attributes like department, clearance level, or patient relationship can grant or restrict access beyond the base role.
- Emergency access procedures. Break-glass protocols allow authorized personnel to access records in emergencies. Every emergency access is logged with justification and reviewed within 24 hours.
- Automatic session timeout. Sessions expire after configurable inactivity periods. We default to 15 minutes for clinical interfaces, 30 minutes for administrative dashboards.
Here's the RBAC middleware pattern we use in our Node.js services:
// RBAC middleware with attribute-based overrides
function authorize(requiredRole, options = {}) {
return async (req, res, next) => {
const user = await getUserWithRoles(req.auth.sub);
// Check base role
const hasRole = user.roles.some(r => r.name === requiredRole);
if (!hasRole) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
// Check attribute-based overrides
if (options.requiredAttributes) {
const userAttrs = new Map(user.attributes.map(a => [a.key, a.value]));
const hasAll = options.requiredAttributes.every(
([key, value]) => userAttrs.get(key) === value
);
if (!hasAll) {
return res.status(403).json({ error: 'Attribute mismatch' });
}
}
// Log access decision for audit trail
await auditLog.write({
userId: user.id,
action: 'access_granted',
resource: req.originalUrl,
role: requiredRole,
timestamp: new Date().toISOString(),
ip: req.ip
});
req.user = user;
next();
};
}
2. Audit Controls
Audit controls require you to record and examine activity in systems that contain or use ePHI. This is not optional logging — it's a structured, tamper-evident record of every interaction with protected data.
- Immutable audit logs with cryptographic chaining. Each log entry includes a hash of the previous entry, creating a chain that makes tampering detectable. We use a write-once storage backend (append-only tables or immutable object storage).
- What to log: Who (user ID, role), what (resource accessed, action taken), when (timestamp with timezone), where (IP, device, location), and outcome (success, failure, reason).
- Retention strategies. HIPAA requires a minimum of 6 years. We retain audit logs for 7 years in compressed, encrypted storage with automated lifecycle policies.
Our audit log schema and write pattern:
// Audit log schema (PostgreSQL)
// Immutable — no UPDATE or DELETE allowed via application code
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
prev_hash BYTEA NOT NULL, -- cryptographic chain
entry_hash BYTEA NOT NULL, -- hash of this entry
user_id UUID NOT NULL,
user_role TEXT NOT NULL,
action TEXT NOT NULL, -- e.g. 'read', 'write', 'export'
resource TEXT NOT NULL, -- e.g. '/api/patients/123/records'
outcome TEXT NOT NULL, -- 'success' | 'failure' | 'denied'
detail JSONB, -- additional context
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Write pattern: append-only, hash-chained
async function writeAuditLog(entry) {
const prevEntry = await db.query(
'SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1'
);
const prevHash = prevEntry.rows[0]?.entry_hash || Buffer.alloc(32);
const payload = JSON.stringify({ ...entry, prevHash: prevHash.toString('hex') });
const entryHash = crypto.createHash('sha256').update(payload).digest();
await db.query(
`INSERT INTO audit_log (prev_hash, entry_hash, user_id, user_role,
action, resource, outcome, detail, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[prevHash, entryHash, entry.userId, entry.userRole,
entry.action, entry.resource, entry.outcome,
entry.detail ? JSON.stringify(entry.detail) : null,
entry.ip, entry.userAgent]
);
}
3. Integrity Controls
Integrity controls protect ePHI from improper alteration or destruction. This covers encryption at rest, encryption in transit, and database-level protections with key rotation.
- Data at rest: AES-256 encryption. Every database, file storage bucket, and backup is encrypted at rest. We use provider-managed encryption (AWS KMS, GCP Cloud KMS) with customer-managed keys for sensitive workloads.
- Data in transit: TLS 1.3. All communication between services, from client to server, and between databases uses TLS 1.3. We disable older TLS versions and weak cipher suites.
- Database-level encryption with key rotation. Encryption keys are rotated on a configurable schedule (we use 90-day rotation). Automated rotation ensures old keys are securely retired without downtime.
Our encryption middleware handles transparent encryption and decryption for sensitive fields:
// Encryption middleware for sensitive fields
const crypto = require('crypto');
class FieldEncryption {
constructor(keyManager) {
this.keyManager = keyManager;
this.algorithm = 'aes-256-gcm';
}
async encryptField(plaintext, context) {
const key = await this.keyManager.getCurrentKey(context);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
ciphertext: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
keyVersion: key.version
};
}
async decryptField(encryptedData, context) {
const key = await this.keyManager.getKey(
context, encryptedData.keyVersion
);
const decipher = crypto.createDecipheriv(
this.algorithm, key,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage in service layer
const patientService = {
async createRecord(data, userId) {
const encrypted = await fieldEncryption.encryptField(
data.ssn, { tenantId: data.tenantId, field: 'ssn' }
);
// Store encrypted blob, never plaintext
return db.patients.create({
...data, ssn_encrypted: encrypted
});
}
};
4. Transmission Security
Transmission security ensures ePHI is protected during electronic transfer. This is the "TLS everywhere" pillar — but with additional considerations for mobile apps and service-to-service communication.
- TLS everywhere, no exceptions. Every HTTP endpoint, WebSocket connection, and API call uses TLS. We enforce HSTS with long max-age and includeSubDomains.
- Certificate pinning for mobile. iOS and Android apps pin the server's TLS certificate (or public key) to prevent MITM attacks, even on compromised networks.
- VPN/PrivateLink for service-to-service. Internal service communication runs over private network links (AWS PrivateLink, GCP Private Service Connect). No ePHI crosses the public internet between microservices.
Our TLS configuration for Node.js services:
// TLS configuration for Node.js services
const https = require('https');
const fs = require('fs');
const tlsConfig = {
// Enforce TLS 1.3 only
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
// Strong cipher suites only
ciphers: [
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_AES_128_GCM_SHA256'
].join(':'),
// Server certificate and private key
cert: fs.readFileSync('/etc/ssl/certs/service.pem'),
key: fs.readFileSync('/etc/ssl/private/service-key.pem'),
// Client certificate verification for mTLS
requestCert: true,
rejectUnauthorized: true,
ca: fs.readFileSync('/etc/ssl/certs/ca-chain.pem')
};
// HSTS headers (set in reverse proxy or middleware)
const hstsHeader = {
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Content-Security-Policy': "default-src 'self'; script-src 'self'"
};
// Apply to all responses
app.use((req, res, next) => {
Object.entries(hstsHeader).forEach(([key, value]) => {
res.setHeader(key, value);
});
next();
});
const server = https.createServer(tlsConfig, app);
server.listen(443);
Architecture Pattern: The Bounded Context Approach
The single most impactful architectural decision for HIPAA compliance is separating PHI-containing services from non-PHI services. This "bounded context" approach limits the blast radius of a security incident and dramatically reduces the scope of your compliance audit.
The core idea: services that handle protected health information live in their own security perimeter with their own database, deployment pipeline, and access controls. Everything else — marketing pages, billing dashboards, analytics — uses standard patterns without the HIPAA overhead.
- API Gateway as the single entry point. All traffic enters through a single gateway that enforces authentication, rate limiting, and request validation. PHI routes are routed to the secure zone; non-PHI routes go to standard services.
- PHI service has its own database. The PHI database is encrypted, access-logged, and backed up to a separate, encrypted bucket. No shared database clusters with non-PHI workloads.
- PHI service has its own deployment pipeline. Deploys to the PHI service require additional approvals, automated security scans, and audit log review before production.
- Non-PHI services use standard patterns. Your marketing site, billing integration, and analytics pipeline don't need the same restrictions. They can use shared databases, standard CI/CD, and conventional access controls.
┌─────────────────────────────────────────────────────────┐
│ API Gateway (Kong/Envoy) │
│ Auth · Rate Limit · Request Validation │
└────────────────┬───────────────────┬────────────────────┘
│ │
┌────────────▼────────┐ ┌──────▼──────────────────┐
│ PHI Service Zone │ │ Standard Zone │
│ │ │ │
│ ┌───────────────┐ │ │ ┌────────────────────┐ │
│ │ Patient API │ │ │ │ Billing Service │ │
│ │ (Node.js) │ │ │ │ (Node.js) │ │
│ └───────┬───────┘ │ │ └────────┬───────────┘ │
│ │ │ │ │ │
│ ┌───────▼───────┐ │ │ ┌────────▼───────────┐ │
│ │ PHI Database │ │ │ │ Shared Database │ │
│ │ (PostgreSQL) │ │ │ │ (PostgreSQL) │ │
│ │ AES-256 enc. │ │ │ │ │ │
│ └───────────────┘ │ │ └────────────────────┘ │
│ │ │ │
│ Audit Log Service │ │ Standard Logging │
│ (immutable store) │ │ (ELK/Datadog) │
└─────────────────────┘ └──────────────────────────┘
│ │
┌────────────▼───────────────────▼────────────────────┐
│ PrivateLink / VPC Peering │
│ (No ePHI on public internet) │
└─────────────────────────────────────────────────────┘
Implementation Checklist
HIPAA compliance is not a one-time event — it's a continuous process. Here's the checklist we follow across all phases of a healthcare SaaS engagement.
Pre-Development
- Sign a Business Associate Agreement (BAA) with your cloud provider (AWS, GCP, Azure all offer BAAs)
- Conduct threat modeling using STRIDE or similar framework
- Document data flows showing where ePHI enters, moves through, and exits your system
- Define role-based access control matrix before writing code
- Establish encryption key management strategy (who rotates, how often, recovery procedures)
During Development
- Follow OWASP Secure Coding Practices guidelines
- Validate and sanitize all input at API boundaries
- Use parameterized queries — never concatenate user input into SQL
- Implement audit logging from day one, not as an afterthought
- Run SAST (Static Application Security Testing) in CI pipeline
- Encrypt sensitive fields at the application layer, not just at the database layer
Pre-Launch
- Third-party penetration testing by a certified assessor
- Automated vulnerability scanning (DAST) on all endpoints
- Review all audit log configurations for completeness
- Test emergency access (break-glass) procedures
- Verify session timeout behavior across all interfaces
- Document incident response procedures and test them with tabletop exercises
Ongoing
- Annual risk assessment and security review
- Workforce security awareness training (documented completion)
- BAAs with all subcontractors who handle ePHI
- Quarterly access reviews and role audits
- Continuous monitoring and alerting on anomalous access patterns
- Regular key rotation and certificate renewal
Common Pitfalls
These are the mistakes we've seen (and made) that cause the most pain during audits and incidents.
Pitfall 1: Treating HIPAA as a One-Time Certification
HIPAA is not PCI-DSS — there's no annual certification. It's an ongoing obligation. Teams that "check the box" during initial development often find themselves non-compliant within months as dependencies update, personnel change, and configurations drift. Build compliance into your CI/CD pipeline and operational processes.
Pitfall 2: Over-Logging (Logging PHI in Audit Logs)
The urge to log everything is understandable, but audit logs that contain PHI create a secondary data store that must itself be protected. Log user IDs, resource identifiers, and actions — not patient names, diagnoses, or medical record numbers. If an auditor finds PHI in your audit logs, that's a separate compliance issue.
Pitfall 3: Ignoring Business Associate Agreements
Every vendor that touches ePHI — your cloud provider, monitoring service, error tracking tool, even your CI/CD platform if it processes source code containing PHI logic — needs a BAA. Using a service without a BAA when it processes ePHI is a violation, regardless of how secure the service is.
Pitfall 4: Using Managed Services Without Understanding Shared Responsibility
AWS RDS encrypts your database, but you're still responsible for key management, access controls, network configuration, and patching. Managed services reduce operational burden, not compliance responsibility. Understand exactly what your provider covers and what falls to you.
Tech Stack Recommendations
Based on our experience across multiple healthcare SaaS products, here's the stack that gives us the best balance of developer experience and compliance coverage.
- Frontend: React/Next.js with strict Content Security Policy (CSP) headers. Next.js gives us server-side rendering for performance and built-in security headers. We enforce CSP at the edge to prevent XSS and data exfiltration.
- Backend: Node.js/Go with middleware for audit logging. Node.js for rapid iteration on API services; Go for high-throughput data processing pipelines. Both have excellent TLS support and mature security ecosystems.
- Database: PostgreSQL with pgcrypto for application-level encryption, or AWS RDS with encryption at rest enabled. PostgreSQL's row-level security policies add another layer of access control at the database level.
- Infrastructure: AWS/GCP with BAA, VPC isolation, and PrivateLink for service-to-service communication. We use Terraform for infrastructure-as-code, ensuring reproducible and auditable infrastructure changes.
- Monitoring: Structured logging with PII redaction. We use a logging pipeline that automatically redacts known PII patterns (SSN, MRN, names) before logs leave the application boundary. Datadog or Grafana for dashboards, PagerDuty for alerting.
Conclusion
HIPAA compliance doesn't require abandoning modern development. It requires deliberate architecture decisions made early, consistent application of technical safeguards, and a culture that treats security as a feature, not a constraint.
The teams that struggle with HIPAA are the ones that treat it as an afterthought — bolting compliance onto a finished product instead of building it into the foundation. The teams that succeed are the ones that understand the principles behind the regulations and apply them thoughtfully to modern patterns.
If you're building healthcare software and want to discuss architecture decisions, we're happy to share what we've learned across our healthcare engagements. Book a consultation and we'll walk through your specific constraints.