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 byuser.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.