Audit Log

Audit Log

Read-only event viewer at /dashboard/audit-log. Shows the current user their own user_events rows written by logUserEvent. It is a per-user self-view, not an admin tool , but on this fork's shared Postgres, that's an application-level restriction (an explicit user_id filter on every query), not a structural one. This is different from the base template, where events lived in each user's own SQLite file and there was no cross-user query to run even by accident , here, a forgotten .where(eq(userEvents.userId, userId)) would be a real cross-tenant leak, not a structural impossibility. See architecture/postgres-port.md for the general tenant-isolation tradeoff this fork accepted.

How events get there

Every authenticated mutation should call logUserEvent after a successful write:

import { logUserEvent } from '@/lib/db/user-events';

// Inside withUserTransaction, after the write:
await logUserEvent(tx, userId, 'item.created', { id });

logUserEvent appends a row to the shared userEvents table, scoped by user_id. The audit-log feature reads these rows back, filtered to the same user.

Scope: logUserEvent only applies to mutations going through withUserTransaction. Shared-DB mutations (billing, credits, admin operations) are not covered , this is intentional, not a gap (see CLAUDE.md's Server Functions rule).

Access

Any authenticated user can open the page; getAuditLogFn calls requireUser() and then listAuditEvents(db, user.id), which filters the shared userEvents table by that user's id , so each user only ever sees their own events, sorted newest first, with event type, timestamp, and payload. There is no compensating enforcement (e.g. a CI lint rule) yet that catches a missing user_id filter on a query like this , it's a documented TODO in CLAUDE.md's Postgres Port Status section, not something already built. An admin wanting another user's events would need direct DB access; no cross-user UI exists by design.

Files:

  • src/features/audit-log/server/audit-log.queries.ts -- fetches the current user's events (requireUser, filtered by user.id)
  • src/features/audit-log/components/ -- event table UI
  • Route: src/routes/_app.dashboard.audit-log.index.tsx

Per-user rate limiting integration

checkUserRateLimit counts userEvents rows (filtered by user_id) to enforce per-user rate limits. Writing to userEvents via logUserEvent is what feeds that count. Both must happen inside withUserTransaction, using the same transaction handle. See Rate limiting.