Error log β
Audience
This article is for GCM platform operators. Church admins should see the Audit log article in Users & Roles for changes they can investigate themselves β the Error log is platform-wide and includes stack traces and request bodies, which are GCM staff-only by design.
The Error log tab (/platform/error-log) is the central triage queue for everything that explodes anywhere in the system. Frontend exceptions caught by the Sentry boundary, edge functions that throw, Vercel API routes that 500, and database triggers that raise β they all converge into platform_error_log and surface here, filterable by source, severity, org, and resolved state, auto-refreshing every 30 seconds.

The five sources β
Every row carries a source enum. The filter dropdown lets you narrow to one at a time:
- Frontend β uncaught React exceptions. Captured by the global error boundary and posted via
/api/log-error. Theurlanduser_agentfields are filled in. - Edge function β Deno edge functions that threw or called
log()with severityerror. Thefunction_namecolumn is set. - Vercel API β Node serverless functions (the
/api/*routes). Like edge functions but with the Vercel runtime context. - DB trigger β raised from inside Postgres, usually by a
RAISE EXCEPTIONin a trigger. Stack trace is the Postgres error context. - Manual β anything
log_errorwas called for explicitly. Used for non-fatal warnings worth surfacing.
Severity β
Three levels, all colour-coded:
warningβ amber. Not failing requests, but loud enough to investigate. RLS denials, slow queries, retried gateway calls.errorβ rose. A request failed. The user saw something broken.fatalβ deep red. A request failed in a way that left state inconsistent. Treat as page-one.
The default filter is "unresolved" β
The page loads with resolved = false so the queue is what is open. Toggle to Resolved to view triaged history, or All for a full view. The Org filter has a special Unattributed (no org) option for rows that fired before a tenant context was established (the most common case for frontend boot-up errors).
The text search hits four columns at once: message, function_name, url, error_code. Use it the way you would use Sentry's search β partial match, case-insensitive.
What a row looks like β
Each row in the table shows:
- When β relative ("3m ago"). Hover for the full timestamp.
- Source β coloured icon + label.
- Severity β pill.
- Org β derived from
context.org_namewhen present, else the first 8 chars oforganization_id, else a dash. - Message β the error message, truncated.
- Where β
function_nameif available, otherwiseurl. - Status β Open or Resolved.
Click any row to open the detail dialog.
The detail dialog β
The detail dialog is the triage workspace. Top half is a key/value grid: function / URL, error code, user (name + email), org + plan, the roles they had, whether they were a platform admin at the time, route, action context, correlation id, user agent.
Two collapsible sections follow:
- Stack trace β the raw stack from the runtime. For DB triggers this is the Postgres error context. For frontend errors it is the JS stack, source-mapped when possible.
- Context (full) β the entire
contextJSONB column pretty-printed. This is where edge functions stash request bodies, query params, and intermediate values. Treat it as sensitive β it can include emails and IDs.
PII in context
The context column is not redacted. It is a raw dump of whatever the failing code captured. Do not paste it into Slack, into a public ticket, or into a Sentry issue. The Error log lives behind platform admin gate precisely so this is safe.
Resolving a row β
The bottom of the dialog has the resolution controls:
- Mark resolved β captures an optional note ("fixed in #123" or "transient, ignoring") and stamps
resolved,resolved_at,resolved_by,resolved_note. The row drops out of the default unresolved view. - Reopen β only visible on already-resolved rows. Clears the resolution columns.
- Delete β destructive. Drops the single row entirely. Useful for personal-data exposures that should not stay in the database.
Resolution is a workflow signal, not a fix. Marking a row resolved does not stop the underlying bug β it just tells your future self the error has been triaged.
Bulk actions β
Two bulk operations live in the table header:
Purge resolved > 30d β
Permanently deletes resolved rows older than 30 days. This is the retention sweep β a triaged row earns no rent past a month. Cheap to run, safe to run regularly. The button reports the count it removed.
Clear filtered β
Deletes every row matching the current filter set. This is dangerous and the UI knows it:
- The button is disabled unless at least one filter narrows the set. Without a filter, the dialog's "matching the current filters" copy would be a lie.
- The confirmation dialog shows the exact count it will delete.
Use this to wipe a noisy class of warnings after you have shipped the fix: filter to source = frontend, message = ChunkLoadError, resolved = unresolved, hit Clear, watch the count zero out.
What the auto-refresh means β
The query refetches every 30 seconds via React Query's refetchInterval. The loading spinner appears inline next to the row count on each refetch β if it is spinning forever, the network is wedged and the existing rows are stale. The Refresh button forces an immediate invalidation.
Common patterns β
A spike of EDGE_FUNCTION warnings from one org β usually a misconfigured webhook hammering an endpoint that returns 429. Open the org and check their channels rows.
Fatal errors with no org β the failure happened before the tenant context resolved. Frontend boot crashes, auth-hook misfires, and edge-fn cold-start failures cluster here. Group by message and the cause is usually a missing env var.
RLS-denial errors from db_trigger β somebody is calling a query without the right role. Cross-reference the user id in context with the audit log to see what they were trying to do.
If a single row needs investigation by an engineer who is not a platform admin, copy the correlation_id and share that β they can grep the edge-function logs in Supabase for the same id without you having to forward the row.
