Skip to content

Compliance Overview

After reading this page you will know which tables to audit, which compliance tags to apply, and how to generate evidence exports for SOC 2, HIPAA, GDPR, and PCI DSS assessments.

RequirementSOC 2HIPAAGDPRPCI DSSBetter Audit feature
Record every data mutationCC7.2§164.312(b)Art. 3010.2ORM auto-capture
Track the acting userCC6.1§164.312(a)(1)Art. 5(2)10.1Actor context
Capture before/after stateCC7.2§164.312(b)Art. 1710.3Before/after snapshots
Protect sensitive fieldsCC6.5§164.312(a)(2)(iv)Art. 253.4Field redaction
Tag entries by frameworkCC7.1§164.530(j)Art. 3010.2Compliance tags
Query and export evidenceCC7.3§164.530(j)Art. 1510.7Query & export
Retain logs for required periodCC7.4§164.530(j)(2)Art. 5(1)(e)10.7Retention policy
Alert on critical eventsCC7.3§164.308(a)(6)(ii)10.6Notify flag

Not every table needs the same level of detail. Focus your audit surface on data that matters to your compliance posture. The snippets below assume an audit instance created with betterAudit() — see the full example at the bottom for a complete setup.

Tables: users, roles, permissions, sessions

Every compliance framework requires tracking who has access and how that access changes. Capture account creation, role assignments, permission grants/revokes, and session events.

src/audit.ts
import { betterAudit } from "@usebetterdev/audit";
import { drizzleAuditAdapter } from "@usebetterdev/audit/drizzle";
const audit = betterAudit({
database: drizzleAuditAdapter(db),
auditTables: ["users", "roles", "permissions", "sessions"],
});
audit.enrich("users", "DELETE", {
label: "User account deleted",
severity: "critical",
compliance: ["gdpr", "soc2", "hipaa"],
notify: true,
});
audit.enrich("roles", "*", {
severity: "high",
compliance: ["soc2"],
});
audit.enrich("permissions", "*", {
severity: "high",
compliance: ["soc2"],
});

Tables: patients, medical_records, prescriptions, appointments

HIPAA requires tracking every access and modification to PHI. Combine compliance tags with field redaction to log the event without storing the sensitive data itself.

src/audit.ts
audit.enrich("patients", "*", {
severity: "high",
compliance: ["hipaa"],
redact: ["ssn", "insurance_id"],
});
audit.enrich("medical_records", "*", {
severity: "critical",
compliance: ["hipaa"],
redact: ["diagnosis", "treatment_notes"],
notify: true,
});

Tables: payments, invoices, subscriptions, refunds

PCI DSS demands a record of all access to cardholder data environments. Redact card numbers and use compliance tags so you can filter and export PCI-specific entries during assessments.

src/audit.ts
audit.enrich("payments", "*", {
severity: "high",
compliance: ["pci"],
redact: ["card_number", "cvv", "card_expiry"],
});
audit.enrich("refunds", "*", {
severity: "high",
compliance: ["pci", "soc2"],
});

Tables: users, profiles, consent_records, data_exports

GDPR requires demonstrating lawful processing and honoring data subject rights. Audit logs provide evidence that deletions, consent changes, and data exports happened as requested.

src/audit.ts
audit.enrich("consent_records", "*", {
severity: "high",
compliance: ["gdpr"],
});
audit.enrich("profiles", "UPDATE", {
label: "Personal data updated",
compliance: ["gdpr"],
description: ({ diff }) => {
const fields = diff?.changedFields.join(", ") ?? "unknown fields";
return `Profile fields changed: ${fields}`;
},
});
audit.enrich("profiles", "DELETE", {
label: "Personal data erased",
severity: "critical",
compliance: ["gdpr"],
notify: true,
});

When an auditor requests evidence, you need to produce filtered, time-bounded exports that match the scope of the control being assessed. Better Audit’s query builder and export engine handle this without custom SQL.

Use .compliance() to pull entries tagged with a specific framework. Tags use AND semantics, so .compliance("gdpr", "hipaa") returns only entries tagged with both:

src/routes/audit.ts
// All GDPR-relevant events in the last quarter
const gdprQuery = audit.query()
.compliance("gdpr")
.since("90d");
// SOC 2 critical events this year
const soc2Query = audit.query()
.compliance("soc2")
.severity("critical")
.since("1y");

Generate downloadable reports scoped to a framework, time range, and severity:

src/routes/audit.ts
// CSV export for SOC 2 assessment — last 12 months, high and critical only
const soc2Evidence = audit.query()
.compliance("soc2")
.severity("high", "critical")
.since("1y");
const response = audit.exportResponse({
format: "csv",
query: soc2Evidence,
filename: "soc2-evidence-2025",
});

For HIPAA audits, export PHI access logs with redacted fields — the redactedFields column in each entry proves sensitive data was excluded from snapshots:

src/routes/audit.ts
const hipaaEvidence = audit.query()
.compliance("hipaa")
.since("1y");
const result = await audit.export({
format: "json",
jsonStyle: "array",
query: hipaaEvidence,
output: "string",
});

Compliance frameworks impose minimum retention periods:

FrameworkMinimum retention
SOC 21 year (typical)
HIPAA6 years
GDPRAs short as necessary (data minimization)
PCI DSS1 year (3 months immediately accessible)

Configure Better Audit’s retention policy to match your strictest requirement, then use the CLI purge command with date filters for framework-specific cleanup:

src/audit.ts
const audit = betterAudit({
database: drizzleAuditAdapter(db),
auditTables: ["users", "payments", "patients"],
retention: {
days: 2190, // ~6 years — HIPAA, the strictest requirement
},
});
Full example: multi-framework compliance setup

A typical compliance setup combines global defaults with table-specific overrides:

src/audit.ts
import { betterAudit } from "@usebetterdev/audit";
import { drizzleAuditAdapter } from "@usebetterdev/audit/drizzle";
const audit = betterAudit({
database: drizzleAuditAdapter(db),
auditTables: ["users", "roles", "payments", "patients", "consent_records"],
retention: { days: 2190 },
});
// Global baseline — every entry gets SOC 2 tagging and low severity
audit.enrich("*", "*", {
severity: "low",
compliance: ["soc2"],
});
// Destructive operations are high severity by default
audit.enrich("*", "DELETE", {
severity: "high",
});
// Identity tables — SOC 2 cares about access control changes
audit.enrich("roles", "*", { severity: "high" });
audit.enrich("users", "DELETE", {
severity: "critical",
compliance: ["gdpr"],
notify: true,
});
// Healthcare tables — HIPAA with redaction
audit.enrich("patients", "*", {
compliance: ["hipaa"],
redact: ["ssn", "insurance_id"],
});
// Payment tables — PCI DSS with redaction
audit.enrich("payments", "*", {
severity: "high",
compliance: ["pci"],
redact: ["card_number", "cvv"],
});
// Consent — GDPR tracking
audit.enrich("consent_records", "*", {
severity: "high",
compliance: ["gdpr"],
});

With this configuration, a users DELETE entry resolves to severity: "critical", compliance: ["soc2", "gdpr"], notify: true — ready for both SOC 2 and GDPR evidence pulls.