Platform guides
05 · Audit + compliance

The audit trail.

Workspace, member, role, policy and PAT operations write append-only audit rows. Read them for support, export them for SOC2, query them for incident response.


1

What gets captured

The workspace audit log records:

  • Member lifecycle — invites created / accepted / revoked (workspace.invite.*), members joined / removed / role-changed (workspace.member.*).
  • Role + policy mutations — custom role and managed-policy create / update / delete (platform.role.*, platform.policy.*).
  • PAT lifecycle — mints, revokes, renames, policy-binding changes (platform.apikey.*).
  • Workspace mutations — created / updated (display name, slug) / deleted (workspace.*).
  • Sensitive reads — member, invite, settings and audit-log reads write rows too (e.g. workspace.audit.read), so the log shows who looked, not just who changed.

What's NOT in workspace audit: per- service activity lives in the service's own log. Auth app changes → GET api.auth.productcraft.co/v1/apps/:app_id/audit-logs. Mail sends → Mail's message log. Social moderation → Social's moderation audit. A unified /v1/workspaces/:slug/audit-feed endpoint exists with a stable shape (see Section 3); today it serves the workspace rows, with the per-service sources arriving as a follow-up.


2

Read it

Requires workspace.audit.read. Newest first, cursor-paginated (limit up to 100 per page).

GET /v1/workspaces/:slug/audit-logs
curl 'https://api.platform-auth.productcraft.co/v1/workspaces/acme/audit-logs?limit=50' \
  -H 'authorization: Bearer <token-or-pak>'
{
  "data": [
    {
      "id":           "...",
      "workspace_id": "<workspace-uuid>",
      "action":       "workspace.member.role_changed",
      "actor_id":     "<owner-account>",
      "actor_type":   "platform_user",
      "resource":     "workspace_membership",
      "resource_id":  "<member-account>",
      "metadata":     { "role_id": "<role-uuid>", "role_name": "admin" },
      "ip":           "203.0.113.4",
      "created_at":   "2026-05-11T..."
    },
    {
      "id":           "...",
      "workspace_id": "<workspace-uuid>",
      "action":       "platform.apikey.created",
      "actor_id":     "<owner-account>",
      "actor_type":   "platform_user",
      "resource":     "platform_api_key",
      "resource_id":  "<pak-id>",
      "metadata":     { "name": "github-actions-staging-deploy",
                        "requested_policy_ids": ["..."], "resolved_policy_ids": ["..."] },
      "ip":           "192.0.2.1",
      "created_at":   "2026-05-11T..."
    }
  ],
  "pagination": { "next_cursor": "...", "has_more": true }
}

3

Query patterns

/audit-logs takes only limit + cursor — unknown query params are silently ignored, so don't rely on server-side filters that aren't documented here. Two patterns cover the common asks:

  • Time-bounded reads — use the unified feed, which accepts since (ISO-8601) and limit up to 500: GET /v1/workspaces/:slug/audit-feed?since=2026-04-01T00:00:00Z.
  • Everything else — filter client-side. Page through and select with jq, e.g. .data[] | select(.action == "workspace.member.role_changed") for every role change, or select(.resource == "platform_api_key") for all PAT lifecycle rows.

4

Export for compliance

For SOC2 / ISO27001 / annual review-style asks, paginate through the log and store it in your own object storage. The feed endpoint is the better export surface: it takes since and up to 500 rows per page.

Export script (bash, jq)
#!/usr/bin/env bash
set -euo pipefail
WS=acme
SINCE="2026-01-01T00:00:00Z"
PAT="${PRODUCTCRAFT_PAT}"
BASE="https://api.platform-auth.productcraft.co/v1/workspaces/$WS/audit-feed"

CURSOR=""
PAGE=0
> audit-export.jsonl

while :; do
  RESP=$(curl -fsS "$BASE?since=$SINCE&limit=500&cursor=$CURSOR" \
    -H "authorization: Bearer $PAT")

  echo "$RESP" | jq -c '.data[]' >> audit-export.jsonl

  HAS_MORE=$(echo "$RESP" | jq -r '.pagination.has_more')
  if [[ "$HAS_MORE" != "true" ]]; then break; fi

  CURSOR=$(echo "$RESP" | jq -r '.pagination.next_cursor')
  PAGE=$((PAGE+1))
  echo "page $PAGE complete"
  sleep 0.5
done

echo "Done. $(wc -l < audit-export.jsonl) rows."
# Need an upper bound too? Filter the export afterwards:
#   jq -c 'select(.timestamp < "2026-07-01T00:00:00Z")' audit-export.jsonl

5

Retention + workspace deletion

Audit rows are append-only — there is no delete or edit endpoint.

Deleting a workspace deletes its audit log with it. The rows live inside the workspace, so workspace deletion (Chapter 1) removes them immediately. If you need the trail for compliance, export it (Section 4) before deleting.


6

Common compliance asks + how to answer them

  • “Who has access to our workspace?” — list members + PATs, plus their roles + policies. The console splits this across two pages: Workspace → Members (people, each with a role selector) and Workspace → API keys (PATs, each showing its bound-policy count and last-used date). For the policy statements behind a binding, use the API: GET .../members + GET .../api-keys.
  • “Show me every action taken by the user we just offboarded.” — export, then jq 'select(.actor_id == "<account>")'. Their platform.apikey.created rows show which PATs they minted — revoke those too.
  • “What changed in the last 30 days?” /audit-feed?since=<30d-ago>. Group by action for the executive view, by actor_id for the per-person view.
  • “Prove we revoked the leaked PAT.” — the platform.apikey.revoked row with resource_id = <pat-id> carries timestamp + actor + the IP that did the revoke.

What's next

You've finished the series

From here: