← All Posts
March 5, 202615 min readEngineering

The Complete Guide to Audit Log Schema Design

How to design an audit log schema that satisfies SOC 2 auditors, scales to millions of events, and actually helps your customers. Includes a field-by-field breakdown, TypeScript examples, and the mistakes we see teams make.

1. Why audit log schema matters

Every B2B SaaS app eventually needs audit logs. An enterprise buyer asks "can we see who did what?" and suddenly you're designing a schema from scratch under deadline pressure.

The schema you choose determines everything downstream: whether your SOC 2 auditor is satisfied, whether your search queries are fast, and whether your customers can actually use the logs you provide.

Get it wrong and you'll be migrating data, re-indexing tables, and apologizing to auditors. Get it right and audit logging becomes a competitive advantage — the feature that closes enterprise deals.

2. The anatomy of an audit event

An audit event answers five questions:

  1. Who performed the action? (the actor)
  2. What did they do? (the action)
  3. What was affected? (the resource/target)
  4. When did it happen? (the timestamp)
  5. What was the result? (success or failure)

This is the "5W" model. Every field in your schema should map to one of these questions. If a field doesn't answer one of these, it belongs in metadata — not at the top level.

3. Required fields (the non-negotiables)

These fields must be present on every single audit event. If any of these are missing, your audit trail has gaps that will concern auditors.

FieldTypeExampleWhy Required
event_idUUID v7"01902a3b-..."Unique identifier; UUIDv7 gives time-ordering for free
timestampISO 8601"2026-03-05T14:30:00Z"When the event occurred. Always store in UTC.
actor.idstring"user_abc123"Who performed the action. Must be traceable to a person.
actor.emailstring"alice@co.com"Human-readable actor identifier. Auditors need names, not just IDs.
actionstring"document.delete"Verb describing what happened. Use resource.verb format.
resource.typestring"document"What type of thing was affected.
resource.idstring"doc_456"Which specific instance was affected.
outcomeenum"SUCCESS" | "FAILURE" | "DENIED"Did the action succeed? Critical for security monitoring.

These fields aren't strictly required but dramatically improve your audit trail's usefulness for security investigations and compliance reviews.

FieldTypePurpose
source_ipstringIP address of the request. Essential for security investigations.
user_agentstringBrowser/client info. Helps detect unauthorized access patterns.
request_idUUIDCorrelates audit events with application logs for debugging.
tenant_idstringMulti-tenant isolation. Each customer sees only their own logs.
metadataJSON objectFlexible key-value pairs for action-specific context (old values, new values, file names, etc.)
geo_locationstringCountry/region derived from IP. Useful for impossible-travel detection.
session_idstringLinks multiple actions to a single user session.
previous_hashstringSHA-256 hash of the previous event. Creates a tamper-evident chain.

5. The complete schema

Here's a TypeScript interface that combines the required and recommended fields into a production-ready audit event schema:

interface AuditEvent {
  // Required fields
  event_id: string;        // UUIDv7 — time-ordered unique ID
  timestamp: string;       // ISO 8601 in UTC
  actor: {
    id: string;            // Internal user ID
    email: string;         // Human-readable identifier
    type?: string;         // "user" | "api_key" | "system"
  };
  action: string;          // "resource.verb" format
  resource: {
    type: string;          // "document", "user", "project"
    id: string;            // Specific instance ID
    name?: string;         // Human-readable name (optional)
  };
  outcome: "SUCCESS" | "FAILURE" | "DENIED";

  // Recommended fields
  tenant_id: string;       // Multi-tenant isolation
  source_ip?: string;      // Client IP address
  user_agent?: string;     // Browser/client info
  request_id?: string;     // Correlation ID
  metadata?: Record<string, unknown>;  // Action-specific context
  previous_hash?: string;  // SHA-256 hash chain link
}

And here's how you'd log an event using the Trailbase SDK:

import { TrailbaseClient } from '@trailbase/sdk';

const trailbase = new TrailbaseClient({
  apiKey: process.env.TRAILBASE_API_KEY,
});

await trailbase.log({
  action: 'document.delete',
  actor: { id: 'user_123', email: 'alice@company.com' },
  resource: { type: 'document', id: 'doc_456' },
  outcome: 'SUCCESS',
  metadata: {
    filename: 'quarterly-report.pdf',
    size_bytes: 2_450_000,
    deleted_by_admin: false,
  },
});

6. Tamper-proofing with hash chains

A SHA-256 hash chain links each event to the previous one. If any event is modified or deleted, the chain breaks — providing cryptographic evidence of tampering.

Event 1: hash = SHA-256(event_data)
Event 2: hash = SHA-256(event_data + event_1_hash)
Event 3: hash = SHA-256(event_data + event_2_hash)
...

// To verify integrity:
// Recompute each hash and compare.
// If any hash doesn't match → tampering detected.

This is the same principle behind blockchain, but without the consensus overhead. Trailbase computes hash chains automatically — every event includes a previous_hash field that you can independently verify.

For a deeper dive, see our post: SHA-256 Hash Chains Explained.

7. Indexing for query performance

Audit logs are write-heavy but read-critical when an incident occurs. Your indexing strategy should optimize for the most common query patterns:

Query PatternRecommended Index
All events for a tenant (paginated)(tenant_id, timestamp DESC)
Events by a specific actor(tenant_id, actor_id, timestamp DESC)
Events on a specific resource(tenant_id, resource_type, resource_id, timestamp DESC)
Events by action type(tenant_id, action, timestamp DESC)
Correlation with app logs(tenant_id, request_id)

Always include tenant_id as the first column in every index. This ensures query isolation in multi-tenant architectures and prevents full table scans.

8. Common mistakes

After reviewing hundreds of audit log implementations, here are the most common mistakes we see:

1. Using auto-increment IDs

Sequential IDs leak information (how many events you have, your growth rate). Use UUIDv7 instead — time-ordered but opaque.

2. Storing timestamps without timezone

Always store timestamps in UTC with explicit timezone offset. "2026-03-05T14:30:00Z" not "2026-03-05 14:30:00". Ambiguous timestamps are useless in cross-timezone investigations.

3. Logging only successful actions

Failed and denied actions are often more important than successful ones. A denied "admin.settings.update" attempt is a security signal. Always log the outcome.

4. Making audit logs mutable

If someone can UPDATE or DELETE audit log rows, the entire trail is suspect. Use append-only tables, hash chains, or a dedicated audit log service.

5. Treating audit logs like application logs

Application logs (console.log, debug output) are for developers. Audit logs are for compliance, security, and customers. Different audiences, different schemas, different retention policies.

6. No tenant isolation in multi-tenant apps

Every query must be scoped to a tenant_id. Without this, a bug or SQL injection could expose one customer's audit trail to another.

9. Audit logs vs application logs

Audit LogsApplication Logs
AudienceAuditors, security, customersDevelopers, SREs
Data modelStructured (actor, action, resource)Unstructured text or JSON
MutabilityImmutable (append-only)Mutable (can be rotated, deleted)
Retention1-10 years (compliance-driven)Days to weeks
Query patternBy actor, resource, time rangeFull-text search, grep
Example"Alice deleted document X""Error: connection timeout on port 5432"
ComplianceRequired for SOC 2, HIPAA, GDPRNot typically audited

10. Retention strategies

Different compliance frameworks mandate different retention periods:

FrameworkMinimum RetentionNotes
SOC 21 yearType II audits review the past 12 months of evidence
HIPAA6 yearsRequired for all security-relevant audit records
GDPRNo fixed periodMust have a lawful basis; delete when no longer needed
ISO 270013 years (recommended)Aligned with certification cycle
PCI DSS1 year3 months must be immediately available for analysis

Use tiered storage to manage costs: hot storage (fast queries, recent data), warm storage (slower queries, older data), and cold storage (archival, compliance-only access). Trailbase handles this automatically with configurable retention tiers.

FAQ

What fields should an audit log event contain?

At minimum: event ID, timestamp (ISO 8601 UTC), actor (id + email), action verb, resource (type + id), and outcome (success/failure). Add source IP, user agent, request ID, and metadata for better auditability.

How do you make audit logs tamper-proof?

Use SHA-256 hash chains where each event includes a hash of the previous event. Store logs in append-only storage. Encrypt sensitive fields with AES-256-GCM. Trailbase does all of this automatically.

What is the difference between application logs and audit logs?

Application logs are for developers (errors, debugging). Audit logs are for compliance and customers (who did what, when). Different audiences, different schemas, different retention.

How long should you retain audit logs?

Depends on compliance: SOC 2 requires 1 year, HIPAA requires 6 years. Most B2B SaaS companies retain 1-3 years with tiered storage to manage costs.

Skip the schema design. Use Trailbase.

Trailbase handles schema design, hash chains, indexing, retention, and compliance reporting. Send your first audit event in 5 minutes.

Get Started FreeView Pricing

Related Posts

SHA-256 Hash Chains Explained

A technical deep dive into how Trailbase guarantees tamper-proof audit logs.

The Enterprise Readiness Checklist

15 things B2B SaaS teams miss when selling to enterprise.