Supabase — Database Trigger Checklist
BEFORE vs AFTER, trigger function standards, timestamps, audit log pattern, SECURITY DEFINER when needed.
Back to Supabase Checklists
0/0 completed
Run this checklist every time you create a database trigger or trigger function.
1. Decision — Should This Be a Trigger?
USE A TRIGGER WHEN:
[ ] An action must always happen after a database event — no exceptions
[ ] The action is too low-level to put in application code (audit logging, timestamps)
[ ] The action must apply even if data is modified by a migration script or admin
DO NOT USE A TRIGGER WHEN:
[ ] The action involves external services (email, webhooks) — use an Edge Function
[ ] The logic is complex business logic — put it in an RPC function instead
[ ] The trigger would create circular dependencies
[ ] The trigger would cause N+1 database calls
2. Trigger Function
3. Trigger Definition
— BEFORE: to validate or modify the row before it is written
— AFTER: to react to a change (create related records, audit log)
(avoids trigger firing on every unrelated update)
4. Common Trigger Patterns
Automatic Timestamps
CREATE OR REPLACE FUNCTION trigger_fn_set_timestamps()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.created_at := now();
END IF;
NEW.updated_at := now();
RETURN NEW;
END;
$$;
CREATE TRIGGER set_timestamps
BEFORE INSERT OR UPDATE ON projects
FOR EACH ROW EXECUTE FUNCTION trigger_fn_set_timestamps();
Audit Log
CREATE OR REPLACE FUNCTION trigger_fn_audit_log()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER -- needs to write audit_logs even if RLS blocks the user
AS $$
BEGIN
INSERT INTO audit_logs (action, table_name, record_id, old_data, new_data, actor_id)
VALUES (
TG_OP,
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP != 'DELETE' THEN row_to_json(NEW) ELSE NULL END,
auth.uid()
);
RETURN NEW;
END;
$$;