Skip to main content

Platform audit logs

The platform-level audit log captures activity across every org on the platform — a much broader scope than the per-org log a national admin sees. Use it for security investigations, compliance reviews, and forensics that span more than one tenant.

What the audit log captures

Each audit log entry records:

  • Action verbCREATE, READ, UPDATE, or DELETE. The verb maps directly to the HTTP method that triggered the request (POSTCREATE, GETREAD, PUT/PATCHUPDATE, DELETEDELETE).
  • Actor — the user who took the action (or "system" for background jobs).
  • Resource type — a string identifying what was acted on (member, chapter, org-settings, dues, compliance, election, forum-moderation, member-profile, member-status-change, member-add-request, membership, admins, storage-config, finances, ai-services-config, sso, platform).
  • Resource ID — UUID of the affected row, when applicable.
  • Org ID — the org context, or empty/Platform for cross-tenant actions.
  • IP address of the request.
  • Timestamp down to the second.
  • Metadata — a JSON payload with the change context: HTTP method, path, response status code, request-body diff (with sensitive fields redacted), and any before/after snapshots when the originating endpoint opted into detailed snapshotting.

The 14 audit log patterns

Audit coverage was expanded significantly in v0.30. There are 14 URL patterns the platform watches, split into two tiers:

Tier 1 — full coverage (logs every HTTP method, including GET reads):

  1. /api/finances/... — every read or write to finance endpoints
  2. /api/organizations/{org}/admins/... — admin lifecycle on a tenant
  3. /api/organizations/{org}/storage-config/... — storage config reads and writes
  4. /api/ai-services/config/... — AI provider config changes
  5. /api/members/{id}/profile/... — every profile view or edit
  6. /api/auth/sso/... — SSO sign-in flows
  7. /api/platform/... — every platform admin action

Tier 2 — write-only coverage (logs POST/PUT/PATCH/DELETE, not GET):

  1. /api/organizations/{org}/members/... — member adds, edits, deletes
  2. /api/chapters/{chapter}/members/... — chapter-scoped member changes
  3. /api/memberships/... — direct membership writes
  4. /api/organizations/{org}/member-add-requests/... — approval workflow
  5. /api/organizations/{org}/status-change-requests/... — status approval flow
  6. /api/organizations/{org}/chapters/... — chapter writes
  7. /api/compliance/... — compliance writes
  8. /api/elections/... — election writes
  9. /api/forums/.../moderate/... — moderation actions
  10. /api/organizations/{org}/settings/... — org settings writes (general, branding, billing, AI, storage, etc.)
  11. /api/organizations/{org}/dues/... — dues writes

(There are slightly more than 14 expressions today — the 14 was the v0.30 baseline. The set grows as new modules ship.)

The split between Tier 1 and Tier 2 is deliberate: for sensitive endpoints (finances, admin lifecycle, SSO, the platform surface), every read matters. For high-volume directory and roster endpoints, logging every GET would generate millions of low-signal rows.

Change-detail capture

Where it's available, the metadata payload includes a before/after diff of the object being changed. This is most useful for UPDATE actions on org settings, admin records, storage configs, and compliance requirements — you can expand the row and see exactly which field changed from what to what.

Not every endpoint opts in to before/after snapshotting today. When the snapshot is missing, the metadata still carries the HTTP body of the request (with password, secret_key, api_key, token, credential, client_secret, and similar fields redacted to bullets), so you can still see what was sent, just not what the prior state was.

What's deliberately not logged

  • Read-only browse traffic on Tier 2 endpoints. A national admin opening the members list does not generate one audit row per member.
  • Sensitive query params are redacted. ?token=..., ?api_key=..., ?secret=..., ?key=..., ?access_token=..., ?refresh_token=... are stripped from the logged URL.
  • Sensitive body fields are redacted. password, password_confirm, api_key, secret_key, token, credential, secret_access_key, client_secret, access_token, refresh_token appear in the body diff as bullets.

Open platform audit logs

Platform → Audit logs.

You'll see a paginated, time-ordered table with the most recent activity at the top.

the platform audit logs table with filters expanded the platform audit logs table with filters expanded

Each row shows:

  • Timestamp (relative — "5 minutes ago" — with absolute timestamp on hover).
  • Actor — name and email (or "system" for background jobs).
  • Action — colored chip (CREATE/READ/UPDATE/DELETE).
  • Resource type and resource ID (truncated to first 8 characters).
  • Org ID — the affected tenant, or "Platform" for cross-tenant actions.
  • IP address of the request.

Click any row to expand it. The expanded view shows the full metadata JSON — HTTP method, path, response status, request body (redacted), and the before/after diff if present.

Filters

The Audit Logs screen exposes four filters today, all combinable:

  • Action — dropdown for CREATE / READ / UPDATE / DELETE.
  • Resource Type — free-text input, case-insensitive contains-match. Membership matches Membership, membership, and any compound type containing those characters.
  • Date From — start of the range.
  • Date To — end of the range.

Pagination is 50 rows per page with a page-size override available via API.

Filters available in the backend but not the UI

The underlying viewset also accepts:

  • org_id — narrow to a single tenant.
  • user_id — narrow to a single actor.

Both work if you call the API directly, but neither is wired into the UI today. Two consequences:

  • "Show me everything user X did in the last 90 days" requires API access or a temporary frontend tweak.
  • "Show me everything that happened in org Y" similarly requires API access or filtering by Org ID in your own head when scanning rows.

These two filters are on the frontend backlog for a future release.

The org-scoped audit log (the one a national admin sees) has a richer filter UI including a metadata keyword search and a distinct-users dropdown — those features didn't make it to the platform-scope view in this release.

Recent Activity rail

The org-admin Logging tab includes a Recent Activity card with the ~30 most-recent events at the top, separate from the filterable main table. The platform-scope view does not have a Recent Activity rail today — the platform table itself defaults to newest-first, which mostly covers the same need, but if you want a glanceable "what just happened across the platform?" widget, it's not on this screen yet.

Expandable detail rows

Click any row to expand. The expanded view renders the metadata JSON in a structured layout — HTTP method, path, response status code, request body with sensitive fields redacted, and (when present) before/after snapshots showing exactly which fields changed and to what.

The detail view is read-only. There's no comment / annotate / tag action — if you need to attach context to a row for a coworker, copy the row ID into a chat or ticket.

Investigation patterns

Suspected misuse of platform privilege

  1. Set a tight date range around the suspected window.
  2. Filter to UPDATE or DELETE.
  3. Filter Resource Type to platform to see only platform-scoped actions.
  4. Scan IP addresses for unfamiliar values.
  5. Expand suspicious rows to see the body diff.

Module flips for a tenant

Filter Resource Type to platform and scan for entries with paths under /api/platform/organizations/{org}/modules/. The body will show is_enabled: true/false and the actor.

Storage config changes

Filter Resource Type to storage-config. Expanded metadata shows region, endpoint, and the masked credential fields (you never see the real key).

Sign-in failures over time

Filter Resource Type to sso. Filter Action to CREATE (SSO sign-ins are POSTs). The metadata will distinguish success from failure via the response status code.

Compliance / election / dues changes for a specific tenant

Use the API directly with org_id={org} plus Resource Type, since the UI doesn't have an org filter yet.

Per-org retention

Retention is controlled per organization, not platform-wide. Each org has a small retention config row with:

  • is_enabled — whether audit logging is collected for this org at all. Default true. Setting it to false stops new rows being written for that tenant; existing rows are kept until trimmed by retention.
  • retention_days — single integer between 1 and 180 days. Default 180 days (the maximum). Older rows are pruned by the retention job.

Configure it via the per-org page on the platform admin side. There is no "2 years / 7 years / permanent" tiered retention model — that was an aspirational claim and does not exist in v0.62.1. If a tenant requires longer retention for regulatory reasons, archive externally before the 180-day cap hits. The pruning job runs daily and is unforgiving.

Org admins can read the retention setting on their own org but cannot change it — see Audit log retention (org admin).

Tamper-resistance

Audit log rows are write-once in practice — there is no API surface that updates or deletes them, and the underlying viewset is read-only.

What that does not mean:

  • There is no INSERT-only database constraint or trigger today. A sufficiently privileged database user could in principle mutate or delete rows directly.
  • There is no cryptographic hash chain across rows; a row can be quietly removed at the database level without leaving evidence in subsequent rows.
  • There is no off-site append-only archive that the application writes to in parallel.

In other words: audit logs are defense in depth, not a legally tamper-proof system. Treat them as a strong signal, not as an evidentiary record sufficient for litigation on its own. For litigation-grade preservation, you need an off-site write-once archive layered on top of the application, which is not built today.

Export

There is no in-product CSV/JSON export for audit logs at the platform scope in v0.62.1. To pull a slice of logs for ad-hoc forensics today:

  • Query the audit_log table directly via the database (read replica recommended for large windows).
  • Use the per-org JSON export to pull an org's audit slice as part of its full data export (see Backups & export).
  • Page through the platform audit log endpoint via the API and capture pages into a file.

A first-class CSV/JSON export at the platform scope is roadmap.

Errors and edge cases

Filter shows zero results when you know something happened. Check:

  • The Resource Type filter is case-sensitive in spelling (it's a contains-match, so partial works) and won't match across types (e.g., Member matches member-profile and member-add-request, but not chapter).
  • The date range filters work in your browser's local timezone; the timestamps in the table are also displayed local. If you're looking for a UTC-noted incident, convert to local first.

Resource ID column is "Platform" instead of a UUID. That row is a cross-tenant action (e.g., a platform admin enabling a module for org X). The actual target may be in the metadata.

A row you expected isn't there. Check:

  • The endpoint that fired isn't on the audit pattern list. Read endpoints in Tier 2 explicitly aren't logged.
  • The retention job ran and trimmed it because it's older than the org's retention_days setting.
  • Audit logging is disabled for the org (rare, but possible — is_enabled = false).

Metadata JSON shows bullets where I expected a value. That field is in the redaction list (password, token, api_key, secret_access_key, etc.) — by design.

Same action appears twice. Two audit middleware paths can both match a request in rare cases. Treat the duplicate as harmless; an engineering ticket exists to dedupe.

Tips

  • Run quarterly platform-admin audits even when there's no incident. Scan the recent module-toggle activity and admin-lifecycle changes; confirm everything was authorized.
  • Archive externally before the 180-day cap if any tenant has longer regulatory retention obligations.
  • Use Resource Type as your primary lens. Even though it's a free-text contains-match, it narrows the result set faster than any other filter.
  • Bookmark filter combinations that you use repeatedly (date + resource type) — the URL captures filter state.
  • Cross-reference with provider logs for SSO investigations. The audit row tells you what GreekManage saw; the IdP's logs tell you what the IdP saw.

Troubleshooting

SymptomLikely causeWhere to look
Resource Type input doesn't seem to filterClick out of the input or wait — it's debounced on typeCheck the resulting query params in the URL
Pagination shows 0 of N pagesEmpty filter result; query is too narrowLoosen date range, broaden resource type
Date pickers look wrongBrowser timezone vs serverBoth UI and DB store UTC; UI displays local
Need to filter by user or orgUI doesn't expose; backend doesCall the API with org_id=... or user_id=...
Logs from before a certain date are missingRetention pruning ranCheck the org's retention_days; archive externally for longer windows

What's not built today

  • CSV / JSON export at platform scope. No bulk export button; API or DB query only.
  • Anomaly / threshold alerts. No "alert when more than N DELETEs from this user in a day" feature.
  • Tagging / annotation on rows. Read-only.
  • Saved filter presets. URL state only.
  • Org and user filters in the UI. Backend supports; UI doesn't surface.
  • Recent Activity rail. Org-admin view has it; platform view doesn't.
  • Append-only DB enforcement or hash chain. Not a legally tamper-proof store.
  • Off-site archive write-through. External archival is a manual operation.
  • Tiered retention (2yr / 7yr / permanent). Max is 180 days, period.

Last verified against v0.62.1 (2026-05-11).