ersistence, versioning, and audit logging. It never blocks the main thread and resolves platform-specific quirks through adapters.
import { Storage } from './storage'; // Abstracted secure storage
import { ConsentPolicy, ConsentState, AuditLogEntry } from './types';
export class ConsentManager {
private state: ConsentState;
private policyVersion: string;
private auditLog: AuditLogEntry[] = [];
constructor(private policies: ConsentPolicy[], private storage: Storage) {
this.policyVersion = '1.0.0';
}
async initialize(): Promise<void> {
const cached = await this.storage.getConsentState();
if (!cached || cached.policyVersion !== this.policyVersion) {
this.state = this.createDefaultState();
await this.promptUser();
} else {
this.state = cached;
}
}
private createDefaultState(): ConsentState {
const defaults: Record<ConsentCategory, boolean> = {
essential: true,
crash_reporting: true,
analytics: false,
advertising: false,
personalization: false,
};
return { categories: defaults, policyVersion: this.policyVersion, timestamp: Date.now() };
}
async isAllowed(category: ConsentCategory): Promise<boolean> {
const policy = this.policies.find(p => p.category === category);
if (!policy) return false;
if (policy.legalBasis === 'essential' || policy.legalBasis === 'legal_obligation') return true;
return this.state.categories[category] ?? false;
}
async updateConsent(updates: Partial<Record<ConsentCategory, boolean>>): Promise<void> {
this.state = { ...this.state, categories: { ...this.state.categories, ...updates }, timestamp: Date.now() };
await this.storage.setConsentState(this.state);
await this.logAuditEvent('consent_updated', updates);
}
private async logAuditEvent(action: string, payload: unknown): Promise<void> {
const entry: AuditLogEntry = { action, payload, timestamp: Date.now(), policyVersion: this.policyVersion };
this.auditLog.push(entry);
await this.storage.appendAuditLog(entry);
}
}
iOS ATT and Android runtime permissions require different initialization sequences. Adapters abstract platform calls and expose a unified requestConsent interface.
export interface PlatformAdapter {
getPlatform(): 'ios' | 'android';
requestATT(): Promise<boolean>;
checkRuntimePermission(permission: string): Promise<boolean>;
requestRuntimePermission(permission: string): Promise<boolean>;
}
// iOS Adapter (pseudo-native bridge)
export class iOSAdapter implements PlatformAdapter {
getPlatform() { return 'ios'; }
async requestATT() {
// Calls native ATTrackingManager.requestTrackingAuthorization()
return await NativeBridge.requestTrackingAuthorization();
}
async checkRuntimePermission(_perm: string) { return true; } // iOS uses ATT, not granular runtime perms
async requestRuntimePermission(_perm: string) { return true; }
}
// Android Adapter
export class AndroidAdapter implements PlatformAdapter {
getPlatform() { return 'android'; }
async requestATT() { return true; } // ATT is iOS-only
async checkRuntimePermission(permission: string) {
return await NativeBridge.checkSelfPermission(permission);
}
async requestRuntimePermission(permission: string) {
return await NativeBridge.requestPermissions([permission]);
}
}
Step 4: Consent-Aware Telemetry Routing
Initialize SDKs only after consent validation. Route events through a middleware that redacts or drops payloads based on active categories.
export class TelemetryRouter {
constructor(
private consent: ConsentManager,
private analyticsSDK: AnalyticsSDK,
private crashSDK: CrashSDK,
private attributionSDK: AttributionSDK
) {}
async initialize(): Promise<void> {
if (await this.consent.isAllowed('crash_reporting')) {
this.crashSDK.init();
}
if (await this.consent.isAllowed('analytics')) {
this.analyticsSDK.init({ consent: true });
}
if (await this.consent.isAllowed('advertising')) {
const attGranted = await this.consent['adapter'].requestATT?.();
if (attGranted) this.attributionSDK.init();
}
}
async trackEvent(event: TelemetryEvent): Promise<void> {
const allowed = await this.consent.isAllowed(event.category);
if (!allowed) {
await this.consent.logAuditEvent('event_dropped', { eventId: event.id, reason: 'consent_denied' });
return;
}
const sanitized = this.redactSensitiveFields(event.payload);
this.analyticsSDK.track(sanitized);
}
private redactSensitiveFields(payload: Record<string, unknown>): Record<string, unknown> {
const sensitive = ['email', 'phone', 'device_id', 'ip_address'];
const cleaned = { ...payload };
for (const key of sensitive) {
if (key in cleaned) delete cleaned[key];
}
return cleaned;
}
}
Step 5: Architecture Decisions and Rationale
- Centralized Consent State: Prevents race conditions where multiple SDKs initialize before consent is resolved. State is cached in secure storage and versioned to trigger re-prompt on policy updates.
- Lazy SDK Initialization: Analytics, attribution, and personalization SDKs initialize only after consent validation. This eliminates background data leakage during app cold start.
- Platform Abstraction: Adapters isolate OS-specific APIs. Adding Android Privacy Sandbox or future iOS consent frameworks requires only adapter updates, not business logic changes.
- Immutable Audit Logs: Consent changes are appended, never overwritten. This satisfies GDPR Article 7 and CCPA §1798.100 verification requirements.
- Consent-Aware Routing: Telemetry middleware drops or redacts events based on active categories. Redaction happens before network transmission, reducing compliance exposure.
Pitfall Guide
-
Hardcoding Consent State Across Sessions
Storing consent in memory or volatile state causes re-prompt loops and audit failures. Consent must persist in secure storage and validate against policy versioning. Without version checks, policy updates silently invalidate prior consent.
-
Ignoring Background Data Collection
Crash reporters, analytics SDKs, and ad networks often initialize during Application.onCreate() or AppDelegate.didFinishLaunching. If they run before consent resolution, they collect device identifiers and network metadata. Solution: defer all third-party initialization to a post-consent lifecycle hook.
-
Treating ATT as Optional Without Fallback Routing
Assuming ATT denial breaks attribution pipelines leads to silent data loss. Architectures must route denied ATT states to privacy-preserving alternatives (e.g., SKAdNetwork, aggregated conversion modeling) rather than disabling attribution entirely.
-
Over-Collecting Under "Legitimate Interest" Without Documentation
GDPR permits legitimate interest, but requires a documented balancing test. Shipping telemetry under this basis without engineering records triggers audit failures. Implement a LegalBasis enum with mandatory justification fields in the consent policy schema.
-
Failing to Implement Data Subject Request (DSR) Workflows
GDPR and CCPA require data export and deletion within 30 days. Apps that lack DSR hooks in their telemetry router cannot fulfill requests. Solution: tag all outbound events with a user_session_id and maintain a reverse-indexed event store that supports DELETE /api/dsr/{userId}.
-
Assuming Third-Party SDKs Are Compliant by Default
SDK vendors update data collection practices without notifying publishers. A compliant architecture must scan SDK manifests, verify data flow diagrams, and enforce consent gates before initialization. Automated CI checks should flag unvetted dependencies.
-
Not Versioning Privacy Policies with App Builds
Policy updates without app version correlation break consent validity. Embed policyVersion in the consent state and trigger a mandatory re-prompt when the embedded version mismatches the current policy hash. Store policy hashes in a secure remote config to prevent client-side tampering.
Best Practices from Production:
- Use async consent resolution during splash/loading screens to mask latency.
- Implement consent fallback states: if resolution fails, default to
essential only.
- Run automated compliance scans in CI/CD that verify SDK initialization order and consent gate placement.
- Cache consent state with TTL-based invalidation to handle policy rollouts without app updates.
- Log all consent decisions to an immutable append-only store for regulatory audits.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP (pre-revenue) | Essential-only routing with deferred analytics | Minimizes compliance overhead while preserving crash reporting and core functionality | Low: $0-5k engineering time |
| Regulated fintech/health | Strict consent gating with immutable audit logs and DSR automation | Meets GDPR/CCPA/HIPAA requirements; prevents regulatory fines and app store removal | Medium: $15-30k engineering + legal review |
| Ad-supported consumer app | ATT-aware routing with SKAdNetwork fallback and consent-aware attribution | Maintains revenue streams despite 35-45% ATT opt-in rates; avoids silent attribution loss | Medium: $10-20k engineering + SDK integration |
| Enterprise internal app | Legitimate interest basis with centralized consent manager and offline DSR | Reduces prompt friction for controlled user base while maintaining audit trails | Low: $5-10k engineering |
Configuration Template
# privacy-compliance-config.yaml
policy:
version: "1.0.0"
effective_date: "2024-01-15"
categories:
- name: essential
legal_basis: contractual_necessity
requires_explicit_consent: false
max_retention_days: 365
sdk_routing:
- crash_reporting
- core_telemetry
- name: analytics
legal_basis: consent
requires_explicit_consent: true
max_retention_days: 180
sdk_routing:
- firebase_analytics
- mixpanel
redaction_keys:
- email
- device_id
- ip_address
- name: advertising
legal_basis: consent
requires_explicit_consent: true
max_retention_days: 90
sdk_routing:
- meta_attribution
- appsflyer
platform_overrides:
ios:
requires_att: true
android:
requires_runtime_permission: false
- name: personalization
legal_basis: consent
requires_explicit_consent: true
max_retention_days: 120
sdk_routing:
- recommendation_engine
consent_manager:
storage_backend: secure_keystore
version_check: hash_based
default_fallback: essential_only
audit_log_retention_days: 730
dsr:
export_endpoint: /api/dsr/export
delete_endpoint: /api/dsr/delete
retention_policy: automatic_after_deletion
verification_method: email_token
Quick Start Guide
- Install dependencies:
npm install @codcompass/consent-core @codcompass/platform-adapters
- Define policies: Copy the YAML template, adjust
legal_basis and sdk_routing for your stack, and load via ConsentManager.loadConfig('privacy-compliance-config.yaml')
- Initialize adapters: Instantiate
iOSAdapter or AndroidAdapter based on Platform.OS, pass to ConsentManager constructor
- Gate SDKs: Replace direct SDK initialization calls with
await consentManager.isAllowed('category') checks; wrap in TelemetryRouter.initialize()
- Verify in CI: Add a pre-commit hook that runs
consent-audit scan --sdk-manifest=./package.json --init-order=./app.tsx to catch initialization violations before merge
This architecture treats privacy as a runtime constraint, not a legal afterthought. Consent state drives data flow, platform adapters isolate OS fragmentation, and audit trails satisfy regulatory verification. Implement once, version continuously, and route intelligently.