Audit expectations
Every state-changing action you take as a platform admin writes to at least one audit log. Impersonation actions write to two simultaneously. Nothing platform admins do is invisible to the target org Owner. Logs are immutable — GET /api/v1/platform/audit-log is the only registered route on the platform audit handler (cloud/internal/platform/audit_handler.go:36-39); DELETE/PATCH are not registered by design.
Platform-side log (platform_audit_log)
365-day retention, configurable via PLATFORM_AUDIT_RETENTION_DAYS (default 365). The daily sweeper at cloud/cmd/server/main.go:448-468 purges older rows. Visible only to platform admins via GET /api/v1/platform/audit-log and the /platform/audit-log page.
The table below splits emitted (live in code today) from documented-but-deferred entries.
Emitted today
| Action | When emitted | Includes | Code |
|---|---|---|---|
platform.login | Every successful platform login | platform_user_id, IP, User-Agent | auth_handler.go:92 |
platform.logout | Explicit logout (cascades impersonation revoke) | platform_user_id | auth_handler.go:157 |
platform.org_created | New customer org provisioned via POST /platform/orgs | actor, target org | org_service.go:148 |
platform.org_disabled | Org suspended via POST /platform/orgs/{id}/disable | actor, target org | org_service.go:171 |
platform.org_enabled | Org re-enabled via POST /platform/orgs/{id}/enable | actor, target org | org_service.go:183 |
platform.org_delete_scheduled | POST /platform/orgs/{id}/delete flipped status to pending_deletion | actor, target org | org_service.go:221 |
platform.impersonation_started | Scoped token minted via POST /platform/orgs/{id}/impersonate | target org, reason, impersonation_id | impersonation_handler.go:94,108 |
platform.impersonation_action | One row per authenticated request made under impersonation (NOT a session summary — every request gets its own row) | impersonation_id, method + path + status | middleware/impersonation.go:147-160 + repository.go:881 |
platform.impersonation_ended | Explicit end, TTL expiry, cascade from org disable/delete-scheduled, or owner toggle | impersonation_id | impersonation_handler.go:174,187 |
Documented but not yet emitted (forward-looking)
These actions appear in the long-term platform-admin design but have no live emitter in code today. Treat any operational pages that depend on them as forward-looking until each emitter ships. See GAP-9-08.
| Action | Long-term intent | Status |
|---|---|---|
platform.login_failed | One row per failed platform login attempt | Failed attempts are counted in failed_platform_logins for rate-limit purposes but no audit row is written. |
platform.password_changed | Platform-admin password change | No password-change endpoint exists yet. |
platform.user_created | A platform admin created another platform user via POST /platform/users | The handler does not emit audit. |
platform.user_disabled | A platform admin toggled enabled=false on another platform user | The PATCH handler does not emit audit. |
platform.org_deletion_initiated / platform.org_deleted | Two-stage hard-delete (initiate + sweep after 7 days) | Code emits platform.org_delete_scheduled once; the post-grace sweeper is unshipped. |
platform.impersonation_blocked | Mint rejected because target org has allow_platform_impersonation = false | The mint handler returns 403 IMPERSONATION_BLOCKED but does not write an audit row for the rejection. |
Org-side log (audit_log)
90-day retention. Visible to the customer’s Owner and Admins via /admin/audit-log. Platform actions appear with a Platform Support badge.
| Action written here | actor_type | Visible with these details |
|---|---|---|
platform.impersonation_started | platform_admin_impersonating | reason, ticket_ref, expiry time |
platform.impersonation_ended | platform_admin_impersonating | end reason |
Org-domain action under impersonation (e.g. user.role_changed, camera.updated, alert_rule.deleted) | platform_admin_impersonating | original action fields + impersonation_id |
org.platform_impersonation_disabled | user (the Owner) | who toggled off, when |
org.platform_impersonation_enabled | user (the Owner) | who toggled on, when |
org.created_by_platform | platform_admin | initial provisioning, platform actor id |
org.disabled_by_platform | platform_admin | reason |
Visibility matrix
| Log | Platform admins | Org Owner | Org Admin | Org Operator | Org Viewer |
|---|---|---|---|---|---|
platform_audit_log | full | no | no | no | no |
Customer audit_log (own org) | via impersonation only | full | full | no | no |
Customer audit_log (other org) | no | no | no | no | no |
What is NOT audited
Read-only platform-side reads do not produce per-request audit rows:
GET /api/v1/platform/orgslisting — no audit row. Acts on platform-owned metadata only, never on tenant content.GET /api/v1/platform/fleet/health— no audit row. Returns aggregate counts only.- Cross-org search — the endpoint is not yet implemented; the documentation page is forward-looking (GAP-9-11).
- Browser navigation inside
/platform/*pages — frontend route changes are not audited. Only backend API calls produce audit.
State-changing calls inside an impersonation session ARE always audited via the dual-write helper (cloud/internal/audit/dual_write.go:47-107). If you find a state-changing path that bypasses dual-write, treat it as a bug and file an incident.
Retention and export
- Platform log: 365 days (configurable via
PLATFORM_AUDIT_RETENTION_DAYS). No CSV export endpoint ships today; export is via direct DB query (GAP-9-12). - Org log: 90 days (configurable via
AUDIT_RETENTION_DAYS). Exportable by org admin via the in-product audit-log export. - Hard deletion of an org would delete its org-side audit log with the rest of the org’s data; the platform-side record (
platform.org_delete_scheduled) survives. The post-grace hard-delete sweeper is unshipped (GAP-9-10).
See also
- How scoped impersonation works — lifecycle that drives most entries here.
- Incident response runbook — when to pull audit logs during an incident.