Setup Activity Log
This skill installs a complete admin activity-log feature into the current project. Covers both auto-captured admin actions (settings changes, invites, etc.) and manual "we shipped X" notes typed by an admin.
Pre-flight checks
Before writing any files, verify the target project:
-
Stack check — read
package.json. The project must have:next(app router — confirm by checking forsrc/app/orapp/)@supabase/ssrand@supabase/supabase-js- shadcn-style UI primitives at
src/components/ui/(card.tsx,button.tsx,input.tsx,textarea.tsx) lucide-reactsonner(for toasts) — if missing, swap toasts for inline error text inpage.tsxzod(for input validation in the API route)
If any are missing, tell the user and stop. Don't half-install.
-
Auth pattern detection — find how admin endpoints check authorization in this project:
- Search for
requireAdmin,isAdmin,requireAdminStrict, or similar insrc/lib/auth.tsorsrc/lib/ - Note the exact import path and function name — you'll substitute this into
route.ts - If the project uses a different pattern entirely (e.g. middleware-only, no helper), ask the user how admin endpoints are gated and adapt
- Search for
-
Profile/role schema — the SQL migration assumes a
profilestable with arolecolumn where admins haverole = 'admin'. Before running it:- Search the existing supabase/ folder for the schema
- If the project uses different names (e.g.
users.is_admin = true), edit the RLS policies intemplates/add_audit_log.sqlaccordingly - Note this for the user
-
Supabase admin client — confirm
src/lib/supabase/admin.tsexists and exportscreateAdminClient. If it's at a different path, update the import intemplates/audit-log.tsandtemplates/route.ts. -
Supabase server client — confirm
src/lib/supabase/server.tsexists with acreateClientasync function. Update import intemplates/audit-log.tsif needed.
Installation
Once pre-flight passes, copy the four template files into place. Read each from templates/ (in this skill's folder) and write to the target paths below. Do not modify the templates other than the small substitutions noted in pre-flight.
| Template | Target path |
|---|---|
templates/add_audit_log.sql | supabase/add_audit_log.sql (or wherever this project keeps migrations) |
templates/audit-log.ts | src/lib/audit-log.ts |
templates/route.ts | src/app/api/admin/audit-log/route.ts |
templates/page.tsx | src/app/admin/activity/page.tsx |
Then update the admin sidebar:
- Find the admin sidebar component (commonly
src/components/admin/admin-sidebar.tsxorsrc/components/layout/admin-nav.tsx) - Import the
Historyicon fromlucide-react - Add a nav entry:
{ href: "/admin/activity", label: "Activity", icon: History }— place it just before the "Settings" entry
Don't instrument any endpoints automatically. The user will add logAuditEvent() calls to specific admin endpoints later as needs arise.
After installation
Tell the user to do exactly two things:
-
Run the SQL migration
- Open https://supabase.com/dashboard → the relevant project
- SQL Editor → New query → paste the contents of the new
add_audit_log.sqlfile → Run - Expect "Success. No rows returned."
-
Refresh and verify
- Visit
/admin/activitywhile signed in as admin - Should see an empty feed and a "Log an update" form
- Type a test entry, hit "Log entry", confirm it appears
- Visit
Then offer to walk them through wiring auto-logging into a specific endpoint. The pattern is one line at the end of any admin handler:
import { logAuditEvent } from "@/lib/audit-log"
await logAuditEvent({
action: "settings.update", // domain.verb
target: "brand_name", // optional human-readable identifier
summary: "Updated brand name to X",
before: oldValue, // optional snapshot
after: newValue, // optional snapshot
})
For client-side admin pages that write to Supabase directly (e.g. a settings page that does .from('app_settings').upsert(...)), they can insert into audit_log directly — RLS allows admins to insert their own rows. Pattern:
const { data: { user } } = await supabase.auth.getUser()
if (user?.email) {
await supabase.from("audit_log").insert({
actor_id: user.id,
actor_email: user.email,
source: "auto",
action: "settings.update",
target: changedKeys.join(", "),
summary: `Updated settings: ${changedKeys.join(", ")}`,
before: oldSnapshot,
after: newSnapshot,
})
}
Action naming convention
Use domain.verb format so the feed groups related events:
settings.update— admin saved settingsuser.invite— new user inviteduser.temp_password— admin issued a temp passwordcontent.upload/content.delete— content managementfeature.deploy— code change shipped (insert withsource: 'auto'so no "manual" badge)manual.note— free-form admin note via the form (set automatically by the API)
Add new actions to ACTION_ICONS and actionLabel() in templates/page.tsx (or have the user do it once in their copy) so they render with proper icons and human labels.