The compliance conversation nobody prepares you for
You're three weeks into a healthcare app build. The client's legal team joins a call. They mention "HIPAA compliance" for the first time — not as a vague goal, but as a contractual obligation with specific technical controls your app needs to pass before launch.
You Google "HIPAA mobile app requirements." You get 47 blog posts written by lawyers, none of which tell you what to actually build. The legal language is precise about outcomes ("ensure the confidentiality, integrity, and availability of all electronic protected health information") and completely silent about implementation ("how?" is your problem).
This post is the bridge between the legal requirements and the code. We'll walk through the four compliance frameworks you'll most commonly encounter building enterprise mobile apps — HIPAA, PCI-DSS, SOC 2, and GDPR — and for each one, we'll map the regulatory language to concrete Flutter implementation decisions.
This is not legal advice. This is engineering guidance for developers who've been told "make it compliant" and need to know what that means for the codebase.
The mental model: compliance is about provable controls
Every compliance framework follows the same pattern at its core:
- Identify sensitive data — what data is the regulation protecting?
- Define controls — what technical and organizational measures prevent unauthorized access, modification, or disclosure?
- Prove the controls exist — documentation, audit logs, and evidence that the controls are implemented and working.
The third point is the one developers underestimate. It's not enough to encrypt data. You need to prove that data is encrypted, that encryption keys are managed properly, that access is logged, and that those logs are tamper-resistant. Compliance is as much about demonstrable evidence as it is about actual security.
Keep this in mind as we go through each framework. At every step, ask: "can I prove this to an auditor?"
HIPAA: Healthcare apps and Protected Health Information
What it protects: Protected Health Information (PHI) — any individually identifiable health information. A patient's name combined with their diagnosis. A user ID linked to medication records. Even a device identifier associated with health data.
Who it applies to: Covered entities (hospitals, insurers, clinics) and their business associates — which includes you, the app developer, if your app touches PHI. Your client will require a Business Associate Agreement (BAA) before you see any real patient data.
The rules that matter for mobile:
HIPAA has two rules relevant to developers: the Privacy Rule (who can access PHI and under what conditions) and the Security Rule (technical safeguards for electronic PHI). The Security Rule has three categories of safeguards:
Administrative safeguards (your process)
- Risk assessment: Before writing code, document what PHI the app handles, where it flows, and what could go wrong. This isn't a formality — auditors ask for it.
- Workforce training: Everyone on the project who could access PHI needs HIPAA training. Yes, even the Flutter developer who "just writes UI code."
- Access management: Document who has access to what. Role-based access control (RBAC) in the app maps directly to this requirement.
Physical safeguards (less relevant for mobile, but still present)
- Device access controls: If the app stores PHI on-device, the device itself needs protection. This translates to: require device-level authentication (PIN/biometric) before the app shows PHI.
- Workstation security: Your development machines that access staging environments with test PHI need encryption and access controls.
Technical safeguards (your code)
This is where it gets concrete.
Access control — unique user identification:
Every user must have a unique identifier. No shared accounts. The app must authenticate users before showing PHI.
// HIPAA requires unique user identification
// This means: no shared logins, no anonymous access to PHI screens
class HipaaAuthGuard {
final AuthService _auth;
final AuditLogger _audit;
HipaaAuthGuard(this._auth, this._audit);
Future<bool> canAccessPhi(String resourceId) async {
final user = _auth.currentUser;
if (user == null) return false;
// Log every PHI access attempt — successful or not
await _audit.log(
event: 'phi_access_attempt',
userId: user.id,
resourceId: resourceId,
timestamp: DateTime.now().toUtc(),
result: 'granted',
);
return true;
}
}Audit controls:
HIPAA requires audit trails for all PHI access. Every time your app displays, modifies, or transmits PHI, that event must be logged with who, what, when, and from where. These logs must be tamper-resistant — sending them to a server-side append-only log is the standard approach.
class HipaaAuditLogger {
final HttpClient _client;
HipaaAuditLogger(this._client);
Future<void> log({
required String event,
required String userId,
required String resourceId,
required DateTime timestamp,
required String result,
}) async {
// Send to server-side audit log — not stored on device
// Server-side log should be append-only and tamper-evident
await _client.post('/api/audit/log', body: {
'event': event,
'user_id': userId,
'resource_id': resourceId,
'timestamp': timestamp.toIso8601String(),
'result': result,
'device_id': await _getDeviceFingerprint(),
'app_version': _appVersion,
});
}
}Transmission security:
PHI in transit must be encrypted. TLS 1.2+ is the minimum. Certificate pinning is strongly recommended — it prevents man-in-the-middle attacks even if a CA is compromised.
// In your HTTP client configuration
class HipaaHttpClient {
static SecurityContext _createContext() {
final context = SecurityContext();
// Pin to your backend's certificate
// This prevents MITM even with a compromised CA
context.setTrustedCertificatesBytes(_pinnedCertBytes);
return context;
}
}Encryption at rest:
PHI stored on the device must be encrypted. SharedPreferences is not encrypted on Android by default. The Dart FFI series covered libsodium — that's one option. For most HIPAA apps, the practical approach is:
- Store PHI data encrypted using AES-256, with keys managed by the platform Keystore/Keychain
- Use
flutter_secure_storage(which wraps Keystore/Keychain) for small values like tokens and keys - Use SQLCipher (via
sqflite_sqlcipherordriftwith encryption) for structured PHI data - Never write unencrypted PHI to disk — including cache files, log files, and debug output
// Secure local storage pattern for HIPAA
class PhiStorage {
final Database _db; // SQLCipher-encrypted database
final FlutterSecureStorage _keyStore;
Future<void> storePatientRecord(PatientRecord record) async {
// The database is already encrypted with a key from the Keystore
await _db.insert('patient_records', {
'id': record.id,
'data': jsonEncode(record.toJson()), // encrypted at rest by SQLCipher
'created_at': DateTime.now().toUtc().toIso8601String(),
});
}
}Automatic logoff:
HIPAA requires that sessions terminate after a period of inactivity. The app must lock or log out the user if they walk away from the device.
class InactivityMonitor {
static const _timeout = Duration(minutes: 5);
Timer? _timer;
final VoidCallback _onTimeout;
InactivityMonitor(this._onTimeout);
void recordActivity() {
_timer?.cancel();
_timer = Timer(_timeout, () {
// Lock the app — require re-authentication to see PHI
_onTimeout();
});
}
}The HIPAA gotcha most developers miss: screenshots. On both Android and iOS, the OS takes a screenshot of your app when it goes to the background (for the app switcher). If PHI is on screen, that screenshot is stored unencrypted in system storage. Fix: set FLAG_SECURE on Android and implement a privacy screen on iOS.
// Android — in MainActivity.kt
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)// iOS — in AppDelegate.swift
func applicationWillResignActive(_ application: UIApplication) {
let blurEffect = UIBlurEffect(style: .light)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = window!.frame
blurView.tag = 999
window?.addSubview(blurView)
}
func applicationDidBecomeActive(_ application: UIApplication) {
window?.viewWithTag(999)?.removeFromSuperview()
}PCI-DSS: Payment data and cardholder information
What it protects: Cardholder data — primary account numbers (PAN), cardholder name, expiration date, service code. Also: sensitive authentication data (CVV, PIN, magnetic stripe data) which must never be stored after authorization.
Who it applies to: Anyone who stores, processes, or transmits cardholder data. If your Flutter app collects credit card numbers, you're in scope.
The pragmatic reality: For most mobile apps, the correct PCI-DSS strategy is scope reduction — don't handle card data at all. Use Stripe, Braintree, or Adyen's mobile SDKs, which tokenize card data on their servers. Your app never sees the PAN, so your PCI scope shrinks dramatically.
But let's talk about what happens when you can't fully avoid scope.
PCI-DSS requirements that affect mobile code
Requirement 3: Protect stored cardholder data
The strongest implementation of this requirement is: don't store it. If your app uses Stripe's SDK, the card number goes directly from the user's input to Stripe's servers via their iframe/SDK. Your backend receives a token, not a PAN. Your mobile app is out of scope for Requirement 3.
If for some reason you must display masked card data (like "ending in 4242"):
// Only ever display masked card data
// The full PAN should never reach your Flutter code
class CardDisplay {
final String last4;
final String brand;
CardDisplay({required this.last4, required this.brand});
String get maskedNumber => '•••• •••• •••• $last4';
}Requirement 4: Encrypt transmission of cardholder data
TLS 1.2+ for all API communication. No exceptions. No fallback to HTTP. Certificate validation must not be disabled, even in development — use a separate dev environment with its own valid certificate instead.
// NEVER do this — seen in real production apps
// HttpClient()..badCertificateCallback = (cert, host, port) => true;
// Instead: proper TLS with no fallback
class PciHttpClient {
final http.Client _client;
Future<http.Response> post(String url, {required Map<String, dynamic> body}) {
assert(url.startsWith('https://'), 'PCI requires HTTPS — no HTTP allowed');
return _client.post(
Uri.parse(url),
body: jsonEncode(body),
headers: {'Content-Type': 'application/json'},
);
}
}Requirement 6: Develop and maintain secure systems
This is the big one for developers. PCI-DSS requires:
- A formal secure development lifecycle (SDLC)
- Code reviews before production deployment
- Vulnerability scanning of your application
- Patching of known vulnerabilities in dependencies
For Flutter, this means:
# Run as part of CI — PCI requires vulnerability scanning
flutter pub outdated # Check for outdated dependencies
dart analyze # Static analysis
# Use tools like Snyk or Dependabot for dependency vulnerability scanningRequirement 8: Identify and authenticate access
Strong authentication for any admin or internal-facing functionality in your app. Multi-factor authentication for remote access. Password complexity requirements (though modern guidance favors length over complexity).
Requirement 10: Log and monitor all access
Every access to cardholder data must be logged. The logging requirements are similar to HIPAA but with specific fields PCI requires: user identification, type of event, date and time, success or failure, origination of event, identity or name of affected data.
PCI-DSS Self-Assessment Questionnaires (SAQs)
The SAQ determines your compliance burden. For most mobile apps using a payment SDK:
- SAQ A: You use a third-party payment processor's hosted UI (Stripe Elements, Braintree Drop-in). Simplest compliance path. ~25 requirements.
- SAQ A-EP: You use a third-party processor but control the payment page. More requirements. ~140 requirements.
- SAQ D: You directly handle card data. Full compliance scope. ~300+ requirements. Avoid this unless you're building a payment processor.
The difference between SAQ A and SAQ D is the difference between a straightforward compliance exercise and a six-month security program. Architect your payment flow to stay at SAQ A.
SOC 2: SaaS and service organization controls
What it protects: Customer data held by service organizations. SOC 2 isn't about specific data types — it's about proving your organization handles customer data responsibly.
Who it applies to: SaaS companies, cloud service providers, and increasingly any company that handles customer data on behalf of others. If your client is a SaaS company and you're building their mobile app, their SOC 2 audit scope may include your app.
How it's different: HIPAA and PCI-DSS are prescriptive — they tell you what to do. SOC 2 is based on Trust Service Criteria — it tells you what outcomes to achieve, and you choose the controls. This means more flexibility but also more ambiguity.
The five Trust Service Criteria
- Security (required) — Protection against unauthorized access
- Availability — System is available for operation and use as committed
- Processing integrity — System processing is complete, valid, accurate, timely
- Confidentiality — Information designated as confidential is protected
- Privacy — Personal information is collected, used, retained, and disclosed in conformity with commitments
Most SOC 2 audits cover Security plus one or two others. For a mobile app, Security and Confidentiality are the most relevant.
What SOC 2 means for your Flutter code
CC6.1 — Logical and physical access controls:
The app must enforce access controls. Users only see data they're authorized to see. Admin functions are restricted to admin users.
// Role-based access control — SOC 2 CC6.1
enum AppRole { user, manager, admin }
class AccessControl {
static bool canAccess(AppRole userRole, String resource) {
const permissions = {
'dashboard': {AppRole.user, AppRole.manager, AppRole.admin},
'reports': {AppRole.manager, AppRole.admin},
'user_management': {AppRole.admin},
'audit_logs': {AppRole.admin},
};
return permissions[resource]?.contains(userRole) ?? false;
}
}CC6.6 — Encryption in transit:
All data transmission uses encrypted channels. Same as HIPAA and PCI — TLS 1.2+, no HTTP fallback.
CC6.7 — Restricting data transmission to authorized users:
API endpoints must verify that the authenticated user is authorized to access the requested resource. This is server-side, but the mobile app should enforce it as a first layer:
// Client-side authorization check before making the API call
// The server must also verify — this is defense in depth
Future<Report?> fetchReport(String reportId) async {
final user = _authService.currentUser;
if (user == null || !AccessControl.canAccess(user.role, 'reports')) {
return null;
}
return _reportService.getById(reportId);
}CC7.2 — Monitoring and detection:
The system monitors for anomalies. For a mobile app, this means:
- Logging authentication attempts (success and failure)
- Detecting unusual patterns (multiple failed logins, access from new devices)
- Alerting on sensitive operations
class SecurityMonitor {
static const _maxFailedAttempts = 5;
static const _lockoutDuration = Duration(minutes: 15);
int _failedAttempts = 0;
DateTime? _lockoutUntil;
Future<LoginResult> attemptLogin(String email, String password) async {
if (_lockoutUntil != null && DateTime.now().isBefore(_lockoutUntil!)) {
await _auditLog('login_blocked_lockout', email: email);
return LoginResult.lockedOut;
}
final result = await _authService.login(email, password);
if (result.isSuccess) {
_failedAttempts = 0;
_lockoutUntil = null;
await _auditLog('login_success', email: email);
} else {
_failedAttempts++;
await _auditLog('login_failed', email: email, attempts: _failedAttempts);
if (_failedAttempts >= _maxFailedAttempts) {
_lockoutUntil = DateTime.now().add(_lockoutDuration);
await _auditLog('account_lockout', email: email);
}
}
return result;
}
}CC8.1 — Change management:
All changes to the system are authorized, tested, and documented. This means:
- Code reviews before merging
- CI/CD pipeline with automated tests
- Deployment approvals
- Version tracking
This isn't code you write in Flutter — it's your engineering process. But SOC 2 auditors will ask for evidence: pull request histories, CI logs, deployment records.
SOC 2 Type I vs Type II
- Type I: Your controls are designed appropriately (a snapshot in time)
- Type II: Your controls operated effectively over a period (typically 6-12 months)
Type II is what enterprise clients care about. It means your processes aren't just documented — they're actually followed, consistently, with evidence.
GDPR: European data protection
What it protects: Personal data of EU/EEA residents. "Personal data" is defined broadly: name, email, IP address, device identifiers, location data, behavioral data — essentially anything that can identify a person, directly or indirectly.
Who it applies to: Any organization that processes personal data of EU residents, regardless of where the organization is located. If your Flutter app has users in Europe, GDPR applies.
The fines: Up to 4% of annual global turnover or €20 million, whichever is higher. This gets clients' attention.
GDPR principles that affect your code
Lawful basis for processing:
You need a legal reason to process personal data. The most common bases for mobile apps:
- Consent: User explicitly agrees (opt-in, not opt-out)
- Contractual necessity: Data is needed to provide the service the user signed up for
- Legitimate interest: Your interest in processing is balanced against the user's privacy rights
The consent mechanism must be specific, informed, and freely given. Pre-checked checkboxes don't count.
// GDPR-compliant consent collection
class ConsentManager {
final SecureStorage _storage;
final AuditLogger _audit;
Future<void> recordConsent({
required String userId,
required ConsentType type,
required bool granted,
}) async {
final record = ConsentRecord(
userId: userId,
type: type,
granted: granted,
timestamp: DateTime.now().toUtc(),
appVersion: _appVersion,
// Store what the user was shown when they consented
policyVersion: _currentPolicyVersion,
);
await _storage.save('consent_${type.name}', record.toJson());
await _audit.log(
event: granted ? 'consent_granted' : 'consent_withdrawn',
userId: userId,
details: {'type': type.name, 'policy_version': _currentPolicyVersion},
);
}
Future<bool> hasConsent(String userId, ConsentType type) async {
final record = await _storage.read('consent_${type.name}');
if (record == null) return false;
return ConsentRecord.fromJson(record).granted;
}
}
enum ConsentType {
analytics,
marketing,
thirdPartySharing,
pushNotifications,
}Data minimization:
Collect only what you need. If your note-taking app doesn't need the user's location, don't request location permission. If your analytics don't need precise device identifiers, use anonymized identifiers.
// Data minimization in practice
class UserRegistration {
// Collect only what's necessary for the service
final String email; // needed for authentication
final String displayName; // needed for personalization
// NOT collected:
// - phone number (not needed for this app)
// - date of birth (not needed)
// - location (not needed)
// - device contacts (not needed)
UserRegistration({required this.email, required this.displayName});
}Right to access (Article 15):
Users can request all data you hold about them. Your app needs a mechanism to trigger this — typically a button in settings that sends a data export request to your backend.
Right to erasure — "right to be forgotten" (Article 17):
Users can request deletion of their personal data. This affects your entire data architecture: you need to be able to find and delete all of a user's data across your backend, analytics, logs, backups, and any third-party services you share data with.
// Account deletion flow — GDPR Article 17
class AccountDeletionService {
Future<DeletionResult> requestDeletion(String userId) async {
// 1. Server-side: queue deletion of all user data
final response = await _api.post('/api/account/delete', body: {
'user_id': userId,
'reason': 'user_requested', // track why for compliance
'timestamp': DateTime.now().toUtc().toIso8601String(),
});
// 2. Client-side: clear all local data
await _secureStorage.deleteAll();
await _database.deleteUserData(userId);
await _cache.clear();
// 3. Clear any analytics identifiers
await _analytics.resetIdentifier();
// 4. Revoke authentication tokens
await _auth.revokeAllTokens();
return DeletionResult.fromResponse(response);
}
}Data portability (Article 20):
Users can request their data in a machine-readable format. Your backend needs an export endpoint that returns the user's data as JSON or CSV.
Privacy by design (Article 25):
Data protection must be built into your app from the start, not bolted on later. This is the GDPR's version of "security as architecture, not a feature." It means:
- Default to the most privacy-protective settings
- Don't enable analytics tracking until the user opts in
- Don't share data with third parties until the user consents
- Use pseudonymization where possible
// Privacy by default — analytics disabled until consent
class AnalyticsService {
bool _enabled = false; // off by default — GDPR privacy by design
void initialize(bool hasConsent) {
_enabled = hasConsent;
if (!_enabled) {
// Ensure no analytics SDK is sending data
_disableAllProviders();
}
}
void trackEvent(String name, {Map<String, dynamic>? properties}) {
if (!_enabled) return; // silently no-op without consent
_provider.track(name, properties: properties);
}
}Breach notification (Article 33/34):
If personal data is breached, you must notify the supervisory authority within 72 hours, and affected users "without undue delay" if the breach is high-risk. This isn't code — it's process — but your app's logging and monitoring systems need to be good enough to detect a breach and determine its scope.
Cross-cutting concerns: what all four frameworks share
Despite their different origins and focuses, these frameworks converge on a set of common technical requirements:
| Requirement | HIPAA | PCI-DSS | SOC 2 | GDPR |
|---|---|---|---|---|
| Encryption in transit (TLS 1.2+) | Required | Required | Expected | Expected |
| Encryption at rest | Required for PHI | Required for PAN | Expected | Expected |
| Access control / authentication | Required | Required | Required | Required |
| Audit logging | Required | Required | Required | Expected |
| Unique user identification | Required | Required | Required | Implied |
| Incident response plan | Required | Required | Required | Required (72h notification) |
| Vulnerability management | Required | Required | Required | Implied (security by design) |
| Data retention limits | Required | Required (PAN) | Expected | Required |
If you build your app's security foundation to satisfy the strictest requirements across all four, you're compliant with all of them. The delta between frameworks is mostly about documentation, process, and the specific data types they focus on.
The compliance implementation checklist
Here's the concrete list of what to build into your Flutter app when compliance is on the table:
Authentication layer:
- [ ] Unique user identification (no shared accounts)
- [ ] Multi-factor authentication (at minimum for admin roles)
- [ ] Session timeout / auto-logoff
- [ ] Account lockout after failed attempts
- [ ] Secure token storage (Keychain/Keystore, not SharedPreferences)
Data protection:
- [ ] TLS 1.2+ for all API communication, no HTTP fallback
- [ ] Certificate pinning for high-security endpoints
- [ ] Encrypted local database (SQLCipher or equivalent)
- [ ] Secure storage for credentials and keys (flutter_secure_storage)
- [ ] No sensitive data in debug logs
- [ ] No sensitive data in app switcher screenshots (FLAG_SECURE)
Audit and logging:
- [ ] Server-side audit log for all sensitive data access
- [ ] Log: who, what, when, where (device), success/failure
- [ ] Tamper-resistant log storage (append-only, server-side)
- [ ] Authentication event logging (login, logout, failed attempts)
Privacy:
- [ ] Consent management with versioned policies
- [ ] Data export capability (right to access)
- [ ] Account and data deletion (right to erasure)
- [ ] Analytics disabled by default, enabled by consent
- [ ] Data minimization — only collect what's needed
Process:
- [ ] Code reviews before production deployment
- [ ] CI/CD with automated security scanning
- [ ] Dependency vulnerability monitoring
- [ ] Incident response plan documented
- [ ] Security documentation for audit readiness
What to tell your client
When a client asks "is our app HIPAA-compliant?" the honest answer is never just "yes." Compliance involves the entire system — the mobile app, the backend, the infrastructure, the organization's policies, the staff training, the BAA with every vendor in the chain.
What you can say is: "The mobile app implements the technical safeguards required by [framework]. Here's the documentation proving each control, the audit logs demonstrating they're active, and the security assessment covering the app's attack surface."
That sentence — and the evidence to back it up — is what separates a $40k app engagement from a $120k one. Not because the code is that different. Because the confidence, documentation, and demonstrable rigor are.
The next post covers what "the evidence to back it up" looks like in practice: security audit artifacts, penetration testing, and the documentation package that makes auditors' lives easy.
This post details the foundation of the Enterprise Flutter Playbook series. It connects directly to Security Audit Artifacts for the delivery side and Secure Enclaves for the key management implementation referenced throughout.