Skip to content
Tech Stack18 June 2026 · 8 min readUpdated 30 September 2026

Audit Logs for B2B SaaS: Schema, Query Patterns, Compliance

Audit log schema, PostgreSQL immutability, query patterns and retention for B2B SaaS, with SQL, primary sources and the ten events to log first.

Audit Logs for B2B SaaS: Schema, Query Patterns, Compliance

What you'll learn

  • Why do B2B SaaS buyers ask for audit logs?
  • What should an audit log schema include?
  • How do you make a PostgreSQL audit trail immutable?
Article contents

An enterprise prospect's security reviewer has a questionnaire open, and one line on it asks whether customers can see who did what inside your product. Before you build anything, write down the six things an audit entry must answer: who acted, what they did, what changed, what it changed on, when, and from where. If your current logs can't answer all six for a single admin action, you have found your first task.

B2B SaaS teams often treat audit logs as a compliance chore. I'd treat them as a product feature with two jobs: give your customers' admins an answer to "who changed this?", and give you the record you need when something goes wrong.

Why do B2B SaaS buyers ask for audit logs?

Buyers ask for audit logs because the log is the record of who did what after something goes wrong, and regulated buyers may be required to keep audit controls on systems that hold sensitive data.

Four large isometric cyan and amber blocks surrounding a central glowing layered data ledger stack on a deep navy platform, viewed from above at 30 degrees

Two primary sources frame the stakes. IBM's Cost of a Data Breach Report 2026 puts the global average cost of a breach at $4.99 million, a 12% increase over the prior year and a record high. A breach investigation starts with the question an audit log answers. And for healthcare customers in the US, the HIPAA Security Rule at 45 CFR 164.312(b) requires covered entities to "implement hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use electronic protected health information."

This suggests a practical reading: if you sell into regulated customers, they have their own reasons to ask for the capability, and a missing audit log is a gap you'll be asked to explain. I can't cite a primary study on how early in the sales cycle reviewers ask, so I won't claim a timing.

What should an audit log schema include?

A production audit log schema records who acted, what they did, what changed, which resource it changed, when it happened and from where, with tenant isolation built into every row.

An isometric overhead grid of geometric column blocks at varying heights on a dark navy plane, the tallest central block glowing bright cyan, viewed at 30 degree isometric angle

FieldTypePurpose
idUUIDPrimary key
tenant_idUUIDMulti-tenant isolation, not optional
actor_idUUID nullableUser or system identity
actor_emailtextReadable identity that survives user deletion
actor_typeenumuser, system, api_key
actiontextStructured event: subscription.plan_changed
resource_typetextsubscription, user, appointment
resource_idtextThe affected row's ID
changesjsonb{"from": "pro", "to": "enterprise"}
metadatajsonbIP address, user agent, request ID
sourceenumui, admin_panel, api, background
created_attimestamptzUTC, millisecond precision

Why store actor_email next to actor_id?

When a user is deleted, their UUID goes cold: an orphaned reference with no name attached. actor_email keeps a readable trace of what they did after the account is gone. If you handle personal data, decide how long that copy may live, because it is personal data too (see the retention section below).

Why store structured diffs instead of descriptions?

{"from": "pro", "to": "enterprise"} is filterable, while "user changed plan" is a log line. The difference shows up when a compliance request asks for every subscription downgrade in a given month. Store before and after values as structured data.

Why add a source field?

Distinguishing ui from api from background is the fastest way to answer "was this a user action or a system job?" while reconstructing an incident. It takes minutes to add and is painful to backfill.

How do you make a PostgreSQL audit trail immutable?

To make a PostgreSQL audit trail immutable, give the application role INSERT and SELECT only, keep table ownership with a separate role the application never uses, and add row-level security so each tenant reads only its own rows.

An isometric amber lock bolt mechanism sealing a flat navy data chamber, with glowing cyan pipelines flowing in from the left but the output side sealed by the prominent amber lock, viewed at 30 degrees

The ownership detail is where many setups fail. The PostgreSQL docs on privileges state that "the right to modify or destroy an object is inherent in being the object's owner, and cannot be granted or revoked in itself," and that an owner can revoke their own ordinary privileges but can always re-grant them. If your application connects as the table owner, no REVOKE protects the table. Separate the roles.

-- A separate owner role holds the table; the app never connects as it
CREATE ROLE audit_owner NOLOGIN;
ALTER TABLE audit_events OWNER TO audit_owner;

REVOKE ALL ON audit_events FROM PUBLIC;
GRANT INSERT, SELECT ON audit_events TO app_role;

-- Row-level security scopes reads and writes to the current tenant
ALTER TABLE audit_events ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_events FORCE ROW LEVEL SECURITY;

CREATE POLICY audit_select_tenant ON audit_events
  FOR SELECT TO app_role
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

CREATE POLICY audit_insert_tenant ON audit_events
  FOR INSERT TO app_role
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

Because app_role has no UPDATE or DELETE privilege and no policy for them, those statements fail. The script runs on PostgreSQL 16, where UPDATE and DELETE return permission errors and an INSERT for another tenant is rejected by the policy. Row-level security has its own bypass rules. The PostgreSQL docs on row security policies say superusers and roles with the BYPASSRLS attribute always bypass it, and table owners normally do too unless the table uses FORCE ROW LEVEL SECURITY. So your admin panel and migration tooling must not share a superuser connection with the read path. Adapt the tenant predicate to however your stack identifies the tenant; the React + Supabase RLS stack page covers one approach.

Some teams also need tamper evidence, meaning a way to prove rows weren't altered by someone with privileged access. Hash chaining is one technique: each row stores a hash of its own data plus the previous row's hash, so a retroactive edit breaks the chain. For most multi-tenant SaaS products, separate roles plus RLS is a reasonable starting point, and hash chaining is worth the extra work when the log itself must stand up to forensic scrutiny. That is my judgment, not a standard requirement.

Callidus, the clinic SaaS I built on React, Firebase and Stripe Connect, runs on Firestore rather than Postgres, so its audit logging is not this SQL. Its case study records audit logging with three retention tiers: 90 days for routine events, one year for financial events and two years for GDPR-sensitive ones, with PATIENT_DATA_ERASED events kept without expiry.

Which queries, indexes and retention rules does an audit log need?

Four queries cover most compliance asks: the workspace timeline, an actor's history, an entity's history and event-type sampling. Four matching indexes serve them, and retention is set by the rules that apply to your customers.

What are the four core queries and their indexes?

-- "Show me what happened in this workspace"
CREATE INDEX ON audit_events (tenant_id, created_at DESC);

-- "What did this user do?"
CREATE INDEX ON audit_events (actor_id, created_at DESC);

-- "Who changed this subscription?"
CREATE INDEX ON audit_events (resource_type, resource_id, created_at DESC);

-- Compliance sampling by event type
CREATE INDEX ON audit_events (action, created_at DESC);

The (tenant_id, created_at DESC) index backs the customer-facing audit view, so it is the one to get right first. I haven't benchmarked these indexes against a specific row count, so measure them with EXPLAIN ANALYZE on your own data instead of trusting a latency figure from a blog.

For text search, when a customer types a term instead of picking from a dropdown, the PostgreSQL pg_trgm module provides GIN operator classes that support trigram-based searches for LIKE and ILIKE.

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX ON audit_events USING gin (action gin_trgm_ops);

When the table grows very large, consider a BRIN index on created_at. The PostgreSQL BRIN documentation says BRIN is designed for very large tables whose columns correlate with physical location, and that such an index is very small. An append-only audit table usually fits that pattern, though you should confirm the correlation on your own data.

How long should you retain audit logs?

Retain audit logs for as long as your strictest applicable rule requires and no longer than you can justify. The table lists what I verified; I couldn't confirm PCI DSS or SOC 2 retention numbers on a primary page this run, so they are left out. Check your framework's current text and ask your auditor.

SourceWhat it says
HIPAA, 45 CFR 164.316(b)(2)(i)Required documentation must be retained for 6 years from creation or last effective date, whichever is later. This covers documentation; the audit-controls text in 164.312(b) quoted above names no log retention period.
GDPR, Article 5(1)(e)Personal data may be kept in identifiable form for no longer than necessary for the purposes of processing.

A storage layout that works: keep recent events in a hot, fully indexed table, move older events to monthly partitions, and export the oldest to cold object storage with a pointer row in Postgres. Monthly partitions help retention because the PostgreSQL docs on partitioning say dropping or detaching a partition is far faster than a bulk operation and avoids the VACUUM overhead of a bulk DELETE.

Never let application code run DELETE on audit records. Run retention roll-off as a scheduled database job or an admin-only migration. An audit trail with an application-layer delete is not an audit trail.

Which ten events should you log first?

This list is my recommendation, built from what a reviewer would want to reconstruct after an incident, not a published standard:

  1. Login success, with method, IP address and MFA status.
  2. Login failure, with reason and lockout state.
  3. SSO configuration changes, including what changed.
  4. Admin role changes: who granted what to whom, and when.
  5. SCIM provisioning: user creation, update and deprovisioning.
  6. API key creation and rotation, with scope, expiry and creator.
  7. Data exports: record count, format and actor.
  8. User impersonation: agent identity and session duration.
  9. Session revocation, with reason and actor.
  10. MFA enrollment and reset.

Many early products capture items 1 and 2 and feel covered. Items 3, 4 and 7 are the ones I'd expect a careful reviewer to probe, because they change who can access data. You don't need all ten on day one, but you do need a read path: customers want to query the log, so plan a tenant-scoped API or a JSON export, and treat a stream to their SIEM as a later step.

In what order should you build it?

  1. Create the audit_events table, the separate owner role and the INSERT and SELECT grants.
  2. Write one logAuditEvent(tenantId, actorId, action, resourceType, resourceId, changes) helper that every mutation path calls, with no direct table writes in business logic.
  3. Add the four indexes.
  4. Expose a tenant-scoped read endpoint filterable by actor, action, resource type and date range.
  5. Ship a customer-facing view in your settings area.

Shipping structured application logs to an observability tool is ops telemetry. The audit trail is a customer-facing feature, and the read endpoint is what your enterprise buyers will use.

Which of the ten events is your product not capturing today? Pick the one that changes who can see data, and add it this week.


Part of the SaaS Security and Compliance: SOC 2, GDPR, and Audit Trails guide.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What should an audit log database schema include?

A production audit log schema should record who acted, what they did, what changed, which resource it changed, when and from where. In practice that means a UUID primary key, a tenant_id for isolation, actor_id and actor_email, a structured action name such as subscription.plan_changed, resource_type and resource_id, a jsonb changes field holding before and after values, a metadata field, a source value that separates ui, api and background jobs, and a UTC timestamptz. The source field is the one many schemas omit, and it speeds up incident reconstruction.

How do you implement an audit trail in PostgreSQL?

Create an append-only audit_events table, give your application role INSERT and SELECT only, and keep table ownership with a separate role the application never uses. PostgreSQL docs state that the right to modify or destroy an object is inherent in ownership, so an application connecting as the owner defeats any REVOKE. Add row-level security scoped by tenant, and use FORCE ROW LEVEL SECURITY so the owner is subject to policies too. Superusers and roles with BYPASSRLS always bypass it, so keep admin tooling on separate connections.

How long should audit logs be retained?

Retain audit logs as long as your strictest applicable rule requires and no longer than you can justify. HIPAA's 45 CFR 164.316(b)(2)(i) requires required documentation to be kept for 6 years, but that covers documentation rather than naming a log retention period. GDPR Article 5(1)(e) says personal data may be kept identifiable no longer than necessary for the purposes of processing. Confirm the current text of any other framework, such as PCI DSS or SOC 2, with your auditor, and document your chosen period.

How do you make audit logs searchable without Elasticsearch?

Use PostgreSQL's pg_trgm extension for text search and ordinary composite indexes for structured filters. The pg_trgm module provides GIN operator classes that support trigram-based searches for LIKE and ILIKE, so a customer typing a term can match action names without a separate search service. For filters by tenant, actor, resource and date, index on tenant_id with created_at, actor_id with created_at, resource_type with resource_id and created_at, and action with created_at. Measure with EXPLAIN ANALYZE on your own data before you trust any latency claim.

What events should a B2B SaaS audit log capture first?

Start with events that change who can access data: admin role changes, SSO configuration changes, API key creation and rotation, and data exports. Then add login success and failure, SCIM provisioning, user impersonation, session revocation and MFA enrollment and reset. This ordering is my recommendation rather than a published standard. Whatever you capture, route every mutation through one logging helper and expose a tenant-scoped read endpoint, because customers will want to query the log themselves.