Drop-in legal compliance toolkit for React apps. Cookie consent, CCPA/GDPR, device fingerprinting, forensic consent logging, analytics, and data rights — everything you need to ship a legally compliant web app.
Built from production code powering a real marketplace with real legal requirements.
| Module | What it does |
|---|---|
| Cookie Consent | Banner + preferences dialog with 4 categories (essential, functional, analytics, marketing) |
| CCPA / Do Not Sell | "Do Not Sell My Personal Information" opt-out with California residency detection (IP geo + email domain) |
| Device Fingerprinting | Canvas, WebGL, audio, font, and hardware signal collection with SHA-256 hashing |
| Forensic Consent Logging | Append-only audit trail with content hashes, exact UI text snapshots, and device metadata |
| Legal Document Versioning | Versioned policies with SHA-256 hashes, material vs minor change types, automatic re-consent |
| Data Rights | GDPR/CCPA data export (JSON download), data retention display, Do Not Sell toggle in settings |
| Analytics | Consent-aware event queue with batched server sync and configurable flush intervals |
| Database Schema | 6 production-ready Drizzle ORM tables (PostgreSQL) |
# Core (required)
npm install compliance-stack react zod
# Optional: database schema
npm install drizzle-orm// app.tsx or layout.tsx
import { configureCompliance } from "compliance-stack";
configureCompliance({
companyName: "Acme Corp",
supportEmail: "support@acme.com",
privacyEmail: "privacy@acme.com",
websiteUrl: "https://acme.com",
// Optional: wire up server callbacks
onSaveCookiePreferences: async (prefs) => {
await fetch("/api/privacy/cookies", {
method: "POST",
body: JSON.stringify(prefs),
});
},
onSubmitDoNotSell: async ({ email, reason }) => {
await fetch("/api/privacy/do-not-sell", {
method: "POST",
body: JSON.stringify({ email, reason }),
});
},
});import { CookieConsent } from "compliance-stack";
function RootLayout({ children }) {
return (
<>
{children}
<CookieConsent
userEmail={currentUser?.email} // For CCPA state detection
cookiePolicyUrl="/legal/cookies"
privacyPolicyUrl="/legal/privacy"
/>
</>
);
}import { DataRightsPanel } from "compliance-stack";
function SettingsPage() {
return (
<DataRightsPanel
onGetPrivacySettings={async () => {
const res = await fetch("/api/privacy/settings");
return res.json();
}}
onUpdateDoNotSell={async (doNotSell) => {
await fetch("/api/privacy/do-not-sell", {
method: "PUT",
body: JSON.stringify({ doNotSell }),
});
}}
onExportUserData={async () => {
const res = await fetch("/api/privacy/export");
return res.json();
}}
/>
);
}import { CookieSettingsLink } from "compliance-stack";
function Footer() {
return (
<footer>
<CookieSettingsLink onClick={() => {
// Re-opens the preferences dialog
// You'll need to lift state or use a global event
}} />
</footer>
);
}6 Drizzle ORM tables ready for drizzle-kit push:
import {
legalDocuments,
legalConsents,
userConsents,
deviceFingerprints,
dataRightsRequests,
analyticsEvents,
} from "compliance-stack/schema";
// Add to your Drizzle schema configlegal_documents — Versioned legal docs (ToS, Privacy, etc.)
├── policy, version, contentHtml, contentHash (SHA-256)
├── pdfKey (object storage snapshot)
├── changeType: "material" | "minor" (triggers re-consent)
└── status: draft → active → retired
legal_consents — Append-only forensic audit log (NEVER update/delete)
├── userId, role, policy, version, contentHash
├── assentType: clickwrap | continued_use | email_acknowledgement
├── assentAction: "checkbox+button", "accept_all_button", etc.
├── assentTextSnapshot: exact label text shown to user
└── forensics: IP, user agent, locale, geo, device fingerprint
user_consents — Mutable consent state (current status per user)
├── consentType: tos, privacy, cookies, marketing_emails, do_not_sell, ...
├── consented: boolean
└── consentedAt / withdrawnAt timestamps
device_fingerprints — Browser fingerprinting for fraud detection
├── deviceHash: salted SHA-256 of all signals
├── signalsJson: screen, timezone, canvas, WebGL, audio, fonts
├── ipAddresses, userAgents, geoCountries (accumulated arrays)
└── riskScore: 0.0 (safe) to 1.0 (high risk)
data_rights_requests — CCPA/GDPR subject access requests
├── requestType: access | deletion | portability | correction | opt_out_sale
├── status: pending → in_progress → completed/denied
└── verificationCode, processedBy, responseFileUrl
analytics_events — First-party event tracking
├── eventType, data (JSONB), userId
└── clientTimestamp, serverTimestamp, sessionId
import { detectCaliforniaIp } from "compliance-stack";
// In your API route:
const { isCaliforniaResident, region, country } = await detectCaliforniaIp(
request.headers.get("x-forwarded-for")?.split(",")[0]
);// Client-side: collect signals
import { collectDeviceSignals, computeDeviceHash } from "compliance-stack";
const signals = await collectDeviceSignals();
const hash = await computeDeviceHash(signals);
// Send { hash, signals } to your server for storageimport { buildConsentRecord, verifyConsentHash } from "compliance-stack";
// When user accepts ToS:
const record = await buildConsentRecord({
userId: "user_123",
role: "user",
policy: "tos",
version: "1.0.0",
effectiveAt: new Date("2024-01-01"),
contentHtml: tosHtmlContent,
assentType: "clickwrap",
assentAction: "checkbox+button",
assentTextSnapshot: "I agree to the Terms of Service",
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
});
// record.contentHash is computed automatically
// Insert into legal_consents table
// Later: verify document hasn't been tampered with
const isValid = await verifyConsentHash(tosHtmlContent, record.contentHash);import { analytics } from "compliance-stack";
import { useCookiePreferences } from "compliance-stack";
// Configure with consent check
const { hasAnalytics } = useCookiePreferences();
analytics.configure({
endpoint: "/api/analytics",
hasConsent: () => hasAnalytics,
});
// Track events (automatically skipped if no consent)
analytics.track("signup", { userId: "123", plan: "pro" });
analytics.pageView("/dashboard");
analytics.error("Something broke", error.stack);Components use CSS custom properties with sensible defaults. Override them in your CSS:
:root {
--cs-bg: #ffffff;
--cs-text: #111827;
--cs-muted: #6b7280;
--cs-muted-bg: #f3f4f6;
--cs-border: #e5e7eb;
--cs-primary: #2563eb;
--cs-accent-bg: #eff6ff;
}
/* Dark mode */
.dark {
--cs-bg: #1f2937;
--cs-text: #f9fafb;
--cs-muted: #9ca3af;
--cs-muted-bg: #374151;
--cs-border: #4b5563;
--cs-primary: #3b82f6;
--cs-accent-bg: #1e3a5f;
}The toggle switch uses a .cs-switch class. Add this CSS for the switch styling:
.cs-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.cs-switch input {
opacity: 0;
width: 0;
height: 0;
}
.cs-switch .cs-switch-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #d1d5db;
border-radius: 24px;
transition: background 0.2s;
}
.cs-switch .cs-switch-slider::before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: transform 0.2s;
}
.cs-switch input:checked + .cs-switch-slider {
background: var(--cs-primary, #2563eb);
}
.cs-switch input:checked + .cs-switch-slider::before {
transform: translateX(20px);
}
.cs-switch input:disabled + .cs-switch-slider {
opacity: 0.5;
cursor: not-allowed;
}configureCompliance({
// Required
companyName: "Acme Corp",
supportEmail: "support@acme.com",
privacyEmail: "privacy@acme.com",
websiteUrl: "https://acme.com",
// Cookie consent
storagePrefix: "acme", // localStorage key prefix (default: companyName lowercase)
consentVersion: "1.0", // Bump to force re-consent
cookieCategories: [...], // Override default categories
// CCPA
ccpaStates: ["CA", "CO", ...], // US states with privacy laws (default: CA,CO,CT,VA,UT)
emailDomainStateMap: { // Map .edu domains to states
"berkeley.edu": "CA",
"ucdavis.edu": "CA",
},
geoIpApiUrl: "...", // IP geolocation endpoint
// Data retention (displayed in UI)
dataRetention: {
activeAccounts: "Retained while active",
inactiveAccounts: "Deleted after 3 years",
paymentRecords: "7 years (legal)",
},
// Analytics
analyticsEnabled: true,
analyticsFlushIntervalMs: 5000,
analyticsEndpoint: "/api/analytics",
// Server callbacks
onSaveCookiePreferences: async (prefs) => { ... },
onSubmitDoNotSell: async ({ email, reason }) => { ... },
onExportUserData: async () => { ... },
onGetPrivacySettings: async () => { ... },
onUpdateDoNotSell: async (doNotSell) => { ... },
});Use this checklist to ensure your implementation covers all requirements:
- Cookie consent banner appears on first visit
- Users can accept all, reject non-essential, or customize
- Cookie preferences persist across sessions (localStorage + server sync)
- Bumping
consentVersionforces re-consent - "Do Not Sell" link appears for CCPA-state users
- Do Not Sell requests are processed within 45 days (CCPA)
- Legal documents are versioned with SHA-256 content hashes
- Consent records are append-only (never updated or deleted)
- Users can download their data (GDPR Article 20)
- Data retention periods are clearly displayed
- Analytics only fire when analytics consent is granted
- Device fingerprints are used for fraud detection, not tracking
- Cookie settings link in footer allows preference changes
User visits site
│
├─ hasConsented() checks localStorage
│ ├─ No → show ConsentBanner (1s delay)
│ │ ├─ Accept All → save all categories
│ │ └─ Customize → PreferencesDialog
│ │ ├─ Toggle categories → Save
│ │ └─ Do Not Sell (CCPA states) → DoNotSellDialog
│ └─ Yes → load stored preferences
│
├─ Preferences saved to:
│ ├─ localStorage (immediate, client-side)
│ └─ Server via onSaveCookiePreferences callback
│
├─ Settings Page:
│ ├─ DataRightsPanel
│ │ ├─ Do Not Sell toggle (IP geolocation detection)
│ │ ├─ Data Export (JSON download)
│ │ └─ Data Retention display
│ └─ Syncs with server via callbacks
│
└─ Database:
├─ legal_documents (versioned, SHA-256 hashed)
├─ legal_consents (append-only forensic audit log)
├─ user_consents (mutable current state)
├─ device_fingerprints (fraud detection)
├─ data_rights_requests (CCPA/GDPR requests)
└─ analytics_events (consent-aware tracking)
MIT