Audit log β
Audience
This article is for GCM platform operators. Church admins see a per-org slice of the same data on Settings -> Activity in their workspace β this page is the cross-tenant superset, restricted to staff.
The Audit log (/platform/audit) is the canonical record of every staff action and every security-relevant event in GCM. Org created, plan changed, refund issued, impersonation started, impersonation ended, sign-in failed, password reset triggered, role assigned β they all land in platform_audit_log with the actor, the target, the timestamp, and a JSONB metadata blob. This tab is how you ask "who did what, when, to whom".

The KPI strip β
Three counts at the top, all live:
- Total events β every row in
platform_audit_log, ever. Mostly useful as a sanity check that the writer is still writing. - Events today β rows since midnight in the platform timezone. A normal day is low thousands; a quiet weekend is hundreds. A sudden zero usually means the audit writer broke.
- Impersonations (7 days) β rows where
action ilike '%impersonat%'in the last week. This is the metric to watch β every impersonation start, end, and failure counts. Spikes here are worth a quick read.
The viewer β
Below the strip sits the unified AuditLogViewer component, also used by the org-scoped Activity page. It supports:
- Date range β from / to with day granularity. Defaults to the last 30 days.
- Action β dropdown of distinct values in
platform_audit_log.action. The list is auto-populated from the DB so it stays current as new actions are added. - Actor β partial email match. Use to scope to one staff user.
- Organisation β by org id. Searches the
metadata->>target_org_idjsonb path so it catches every event that mentions an org, even when the row's ownorganization_idis null.
Filters compose; clearing one keeps the others active.
Reading a row β
Each row in the table shows:
- When β exact timestamp.
- Action β the verb. The full set is documented in the migration that adds the column constraint; common ones include
org_created,plan_updated,refund_processed,subscription_overridden,impersonation_started,impersonation_failed,email_template_updated,auth_login_failed. - Actor β email of the staff user who triggered the row. Edge-function and cron rows show
system@geniuschurchmanager.com. - Target β the affected org name (resolved from the metadata) or "platform" for platform-wide rows.
- Metadata β truncated JSONB preview.
Click any row to expand the metadata. Refunds, for instance, store the original amount, the refund amount, the manual method (if off-platform), and the reason text. Subscription overrides store the before / after values for every changed field.
Common investigations β
"Who refunded that $79 payment from First Church on Tuesday?" Filter action = refund_processed, set the date range to Tuesday, filter organisation = First Church. The actor column is your answer.
"Has anyone from our team logged into Second Baptist's workspace this month?" Filter action = impersonation_started, set the date range to "the month", filter organisation = Second Baptist. Each row carries the reason in metadata β read those next.
"Did the customer's plan really change last week or are they imagining it?" Filter action = plan_updated, filter organisation = <their org>. If a row exists, the metadata shows the before/after plan id and who did it. If no row exists, the customer is misremembering β but check subscription_overridden too in case the change came through the billing-ops edit dialog.
"How many failed sign-ins in the last 24 hours?" Action auth_login_failed, date range "yesterday to today". Cross-reference the actor with the Users tab to see if one account is being targeted.
Action reference (the important ones) β
| Action | When it fires | Key metadata |
|---|---|---|
org_created | New org provisioned | org_name, org_slug, plan_id, created_by_admin |
org_deleted | Hard delete | org_name, org_slug, member_count, reason |
plan_updated | Inline plan change on the Organisations tab | target_org_id, previous_plan, new_plan |
subscription_overridden | Edit subscription dialog | target_org_id, before, after, reason, email_sent |
renewal_triggered | Manual renewal (charge mode) | target_org_id, mode, reason, result |
refund_processed | Refund modal | payment_history_id, amount, refund_type, reason |
impersonation_started | Open account from org sheet | target_org_id, org_name, reason |
impersonation_ended | Return to platform | target_org_id |
impersonation_failed | RPC rejected the start | target_org_id, reason, error |
email_template_updated | Save in Email Templates tab | slug, before, after |
landing_content_updated | Save in Landing tab | section, content_key, locale, before, after |
branding_settings_saved | Save in Branding tab | keys |
auth_login_failed | Bad password or unknown email | email, ip, user_agent |
The list is not exhaustive β new actions are added with each feature.
Retention β
platform_audit_log is not purged automatically. Rows live forever until someone deletes them, and we deliberately do not expose a UI to do that β the audit trail is the audit trail. If you need to reduce the table size for a migration, write a migration that archives to a cold-storage table and document it in docs/runbooks/.
What you cannot do here β
The viewer is read-only. There is no "edit", no "annotate", no "mark important". If you need to attach context to a row, write a new row by performing the action it describes (e.g. add a Slack comment on the ticket the audit row references, or open the linked Sentry issue). Treat the audit log the way a flight recorder works: read-only, write-once, never the place you go to clear a problem.
