Audit Log for Admin Actions
Admin Safety · ADM-04 · Priority: P1
Why It Matters
Without an audit log, you cannot answer the most basic security questions: Who deleted that user? When was the role changed? Who exported the data? When a breach or internal incident occurs, the absence of an audit trail turns investigation into guesswork.
AI code generators never create audit logs. They build admin panels with CRUD operations — create, read, update, delete — but record nothing about who did what, when, or why.
For any SaaS pursuing enterprise customers, SOC 2 Type II requires audit logging for privileged actions. Missing audit logs are an automatic compliance failure.
Priority: P1 — Required for incident response, compliance, and internal accountability.
Affected Stack: Supabase, Next.js, any framework with admin functionality
The Problem
// ❌ Admin action with no audit trail
export async function POST(req: Request) {
const { userId } = await req.json();
await supabase.from('users').delete().eq('id', userId);
return new Response('Deleted'); // Who deleted? When? Why? No record.
}
The Fix
Create an append-only audit log table and write to it for every admin action.
-- ✅ Audit log table (append-only)
CREATE TABLE admin_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
admin_id UUID NOT NULL REFERENCES auth.users(id),
action TEXT NOT NULL, -- 'user.delete', 'role.change', 'data.export'
target_type TEXT, -- 'user', 'subscription', 'organization'
target_id TEXT, -- ID of affected record
details JSONB DEFAULT '{}', -- Additional context
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Make it append-only: no UPDATE or DELETE allowed
ALTER TABLE admin_audit_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY "No modifications" ON admin_audit_log
FOR ALL USING (false);
CREATE POLICY "Insert only for service role" ON admin_audit_log
FOR INSERT WITH CHECK (true);
// ✅ Admin action with audit logging
export async function POST(req: Request) {
const admin = await getVerifiedAdmin(req);
const { userId } = await req.json();
// Perform the action
await supabase.from('users').delete().eq('id', userId);
// Log it (append-only, cannot be tampered with)
await supabaseAdmin.from('admin_audit_log').insert({
admin_id: admin.id,
action: 'user.delete',
target_type: 'user',
target_id: userId,
ip_address: req.headers.get('x-forwarded-for'),
});
return new Response('Deleted');
}
What to log:
- User deletion, suspension, role changes
- Data exports and bulk operations
- Billing overrides and manual adjustments
- Configuration changes
- Impersonation sessions (start + end)
- Failed admin auth attempts
References
Related Checks
- MFA for Admin Roles — ADM-13
- No Client-Side-Only Role Checks — ADM-03
- Exposed Debug/Admin Routes — ADM-08
Is your app safe? Run Free Scan →