Blog

How to add an audit log to your SaaS in 30 minutes

This is the tutorial version of a promise we make on the homepage, so let's hold ourselves to a clock. Starting point: a Node backend (any framework), a user object with an id, and 30 minutes. End point: eight security-relevant events flowing, and a customer-facing audit page your next security questionnaire can screenshot.

If you'd rather understand the schema thinking first, read the actor/action/target post — but you don't need it to follow along.

Minutes 0–3: install and initialize

Terminal + .envsetup
# ~3 min — install and configure
npm install @softechlog/node

# .env — from the dashboard API Keys page after sign-up
SOFTECHLOG_SECRET_KEY=stl_sk_xxxxxxxxxxxx
lib/softechlog.tsone instance
// lib/softechlog.ts — ~1 min
import { Softechlog } from '@softechlog/node';

export const log = new Softechlog({
  secretKey: process.env.SOFTECHLOG_SECRET_KEY!,
});

Sign-up and project creation are part of the three minutes — the onboarding wizard hands you the secret key and then waits for your first event, which is convenient for step 4.

Minutes 3–18: the eight events that matter

Resist the urge to instrument everything. Eight events at your backend's choke points answer the overwhelming majority of "who did what" questions:

your service layer8 × track()
// ~15 min — one line at each choke point in your service layer.
// These eight cover most of what customers and auditors ever ask for:

// 1+2. Authentication (in your login handler / auth callback)
log.track({ actor, action: 'auth.login.succeeded', metadata: { method: 'google' } });
log.track({ actor: { id: attemptedEmail }, action: 'auth.login.failed' });

// 3+4. Membership (invite + removal handlers)
log.track({ actor, action: 'member.invited', target: ws, metadata: { invited_email, role } });
log.track({ actor, action: 'member.removed', target: ws, metadata: { removed_email } });

// 5. Authorization changes (role-change handler)
log.track({ actor, action: 'member.role.changed', target: ws, metadata: { from, to } });

// 6+7. Credentials (API-key create/revoke handlers)
log.track({ actor, action: 'api_key.created', target: { type: 'api_key', id: key.id } });
log.track({ actor, action: 'api_key.revoked', target: { type: 'api_key', id: key.id } });

// 8. Data movement (export handler)
log.track({ actor, action: 'data.exported', metadata: { format, rows: count } });
lib/softechlog.tshelper
// Tip: one helper keeps actors consistent everywhere
const asActor = (user: User) => ({
  id: String(user.id),
  name: user.name,
  email: user.email,
});

Two properties make this safe to sprinkle liberally: track() never throws, and it's fire-and-forget — a logging hiccup can't fail a customer request. (On serverless, await log.flush() before returning; details in the Next.js guide.)

Minutes 18–23: the feed token endpoint

The audit page runs in your customer's browser, and browsers never get the secret key. Instead your server mints a short-lived token scoped to exactly one user:

api/activity-tokenserver
// ~5 min — the read side: a feed token endpoint
// app/api/activity-token/route.ts (or an Express route)
import { log } from '@/lib/softechlog';

export async function GET(req: Request) {
  const user = await requireUser(req); // your auth
  const { token } = await log.feedToken({ actorId: String(user.id), ttlSeconds: 3600 });
  return Response.json({ token });
}

The scope decision (actorId) comes from your auth, not from anything the client sends — that's what makes the page IDOR-proof by construction.

Minutes 23–28: the audit page

settings/activity.htmlany framework
<!-- ~5 min — the audit page itself. Works in any framework. -->
<script src="https://softechlog.com/feed.js"></script>

<h2>Account activity</h2>
<softechlog-feed id="audit"></softechlog-feed>
<script>
  fetch('/api/activity-token')
    .then((r) => r.json())
    .then(({ token }) => document.getElementById('audit').setAttribute('feed-token', token));
</script>

<softechlog-feed> is a Web Component — loading, empty, error, and pagination states are handled. Theming attributes are in the component docs when you want it to match your UI.

Minutes 28–30: verify

Terminalend-to-end check
# ~2 min — verify end to end
# 1. Perform a tracked action in your app (invite someone, rotate a key)
# 2. Watch it arrive: dashboard → Events → Live
# 3. Load your new audit page — the event is in the feed
# 4. Prove export works while you are at it:
curl -H "Authorization: Bearer $SOFTECHLOG_SECRET_KEY" \
  "https://api.softechlog.com/v1/events/export?format=csv" -o audit.csv

What you just avoided

The half hour above replaces the schema design, ingest validation, cursor pagination, scoped-read security model, retention job, and export endpoint you'd otherwise build — about 10 engineering weeks by our line-by-line estimate. The free tier (10k events/month) is enough to run this whole tutorial and then some; nothing above required a paid plan.

Where to go next: wire your remaining sensitive actions incrementally (billing changes and deletions are usually the next two customers ask about), add browser auto-capture if support wants full session timelines, and read what an audit trail needs before SOC 2 if there's a questionnaire on the horizon.

Stuck anywhere in the 30 minutes? Beta partners get hands-on onboarding.

Request Beta Access