Somewhere between your first enterprise-ish customer and your first security questionnaire, every B2B SaaS founder hears the same request: "Can we see a log of who did what in our account?" And every founder has the same first reaction: that's an events table and a page that renders it. A sprint, tops.
I want to walk through what actually happens after that sprint, with real code — because the decision to build or buy an audit log is only easy if you're honest about the full scope. I run an audit log API company, so discount my bias accordingly. The numbers below are yours to check.
Week 1: the events table that feels done
-- Week 1: "we just need an events table" CREATE TABLE audit_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid REFERENCES users(id), action text, payload jsonb, created_at timestamptz DEFAULT now() );
This works. It demos well. You sprinkle a few INSERTs into your invite and billing handlers, build a page that lists rows newest-first, and close the ticket. If your product never grows past this, genuinely: keep it. You don't need an audit log API for a table nobody queries.
Month 3: what the table becomes
The problems don't arrive as one big failure. They arrive as tickets:
- "The log says 'user deleted a file' but which file?" — you logged the action, not the target. Now you need target type, id, and name (denormalized, because targets get renamed and deleted).
- "Why does the log show a deleted user as 'Unknown'?" — foreign keys to your users table break history the moment someone leaves. You need actor snapshots.
- "Support needs to see everything this user did around 2pm Tuesday" — now you need session grouping, IP, user agent, and a time-window query that doesn't table-scan.
- "The activity page takes 9 seconds for our biggest customer" — offset pagination on a hot table. You're rewriting it with cursors.
- "Legal says we can't keep this data forever" — retention isn't a cron job you write once; it's a job you monitor forever.
Which is how the "quick events table" converges, at every company, to roughly this:
-- Month 3: what it actually becomes
CREATE TABLE audit_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL, -- multi-tenant isolation
actor_id uuid NOT NULL, -- who (FK to a snapshot table,
-- because users get deleted)
action text NOT NULL, -- constrained: resource.verb
target_type text, -- what it happened to
target_id text,
target_name text, -- denormalized: targets get renamed
metadata jsonb, -- capped, validated
session_id uuid, -- grouping for support timelines
ip_address inet,
user_agent text,
occurred_at timestamptz NOT NULL, -- client time ≠ ingest time
ingested_at timestamptz DEFAULT now()
);
CREATE INDEX ON audit_events (project_id, occurred_at DESC);
CREATE INDEX ON audit_events (project_id, actor_id, occurred_at DESC);
CREATE INDEX ON audit_events (project_id, action, occurred_at DESC);
-- …and the partial indexes you add after the first slow-query pageAnd the schema is the easy half. The rest is an ingest path that validates and never blocks your request cycle, a read API with cursor pagination and filtering, credential scoping so a customer-facing feed can't leak other users' events, export for the customers who ask (they will — usually during a security review), and the retention purge. We priced this line-by-line on our build-vs-buy cost page: it comes out to roughly 10–12 engineering weeks (~$46k loaded) for the first production version, plus 10–15% a year in maintenance.
The buy side, for contrast
Any decent audit log API collapses all of the above into one call from your backend:
import { Softechlog } from '@softechlog/node';
const log = new Softechlog({ secretKey: process.env.SOFTECHLOG_SECRET_KEY });
// The entire integration:
log.track({
actor: { id: user.id, name: user.name, email: user.email },
action: 'billing.plan.upgraded',
target: { type: 'workspace', id: workspace.id, name: workspace.name },
metadata: { from: 'free', to: 'growth' },
});Storage, indexes, pagination, retention, per-user read tokens, and export come with it. Softechlog's paid plans are $29–$99/month — a year of the top plan costs less than one week of the build. The economics aren't subtle, which is exactly why you should be suspicious and check the other column.
When building is actually right
Buying is not always the answer. Build your own audit log when:
- Activity data is your product. If you're selling analytics, observability, or compliance tooling itself, this is core competence — own it.
- You have hard residency or air-gap requirements. Some regulated buyers won't accept a third party in the event path at any price.
- Your volume is extreme. Past a certain events/second, infrastructure cost dominates engineering cost and the vendor margin stops making sense.
If none of those describe you, the build option is really a decision to spend your scarcest resource — roadmap weeks — on plumbing your customers assume you already have.
The founder heuristic
Ask one question: will a customer ever pay you more because your audit log is home-grown? If yes, build. If they'll only ever notice whether it exists, works, and exports — buy it, ship the feature this week, and spend the 10 weeks on something that shows up in your pricing page.