01The shape

One table — activity_log — captures every state change. One trigger function — tg_write_activity_log(tenant_col) — runs on every tenant-scoped table. The trigger takes the tenant column name as an argument so the same function is reused across approvals, knowledge_entries, projects, team_instances, team_members, teams_channels, invitations and the others — one source of truth for "what happened, when, by whom".

Each row carries:

  • tenant_id — the company the action affected
  • actor_id — the user (or service_role) who performed it
  • op — INSERT / UPDATE / DELETE
  • table_name + row_id — what was changed
  • before + after — the row state on either side of the change (jsonb)
  • occurred_at — server-side timestamp

02What it looks like

A single Friday afternoon on the Acme demo project, viewed in operator mode (timestamps simplified, IDs trimmed):

14:02INSERTapproval_pendingapprovalsMarie Dewulf · agent
14:02INSERTusage_eventusage_eventsMarie Dewulf · agent
14:08UPDATEapproval_decidedapprovalskoen@archevia.be · admin
14:08INSERTknowledge_entry_createdknowledge_entrieskoen@archevia.be · admin (via trigger)
14:24INSERTapproval_pendingapprovalsSara Janssens · agent (orchestrator)
14:24INSERTapproval_pendingapprovalsYusuf Demirci · agent (child)
14:24INSERTapproval_pendingapprovalsKhadija El Bouazzaoui · agent (child)
15:11UPDATEteam_member_addedteam_memberskoen@archevia.be · admin

The same shape covers everything. Approvals being created and decided, team members being added, knowledge entries being materialised, integrations being provisioned. One place to look when the question is "what did the team do this morning."

03Why a trigger, not application code

Application code can forget to log. A trigger can't. The platform's RLS posture deliberately makes most tables not-writable from the authenticated path; writes go through SECURITY DEFINER RPCs (decide_approval, onboard_company, set_project_pricing, ...) — and the trigger fires inside those RPCs the same as it would on a direct write.

The path is single: write happens → trigger fires → audit row exists. If the audit row doesn't exist, the write didn't happen. There's no alternative path that bypasses the log.

04Cross-tenant: gdpr_operations

The activity log is tenant-scoped — when a tenant is deleted (GDPR Article 17), the activity log for that tenant goes with it. That's the right behaviour for tenant data, but the wrong behaviour for the act of deletion itself: the audit of "we deleted a tenant" needs to outlive the tenant.

So a separate cross-tenant table — gdpr_operations — captures every export and delete with a company_name_snapshot, the operator who triggered it, the timestamp, the legal basis stated. Permanent by design. Visible only to platform admins. The audit row outlives the deleted tenant — that's the legal point.

2026-04-30EXPORTarticle_15_+_20gdpr_operationsplatform-admin · note: customer request
2026-05-12DELETEarticle_17_hard_deletegdpr_operationsplatform-admin · note: 30-day purge after pilot ended

05What you can do with it

  • Operator history — "what did the team do this week?" — filter by tenant, by actor, by table, by operation kind.
  • Compliance evidence — when an auditor asks how a vendor decision was reached, the log links the proposal, the reviewer's approval, the materialised decision, the timestamp.
  • Debugging — when a row's state doesn't match expectations, the before/after history is in the log without needing to reconstruct from event sourcing.
  • Customer-side transparency — once SSO is wired and a customer's lead consultant is in tenant_operator role, they get the same audit view scoped to their company.

06Compliance fit

  • GDPR Article 30 (records of processing): the log is the record, scoped to processing operations.
  • SOX / IFRS internal controls: every state-changing action is captured with an actor, a timestamp, and a before/after; segregation of duties is enforceable on the read.
  • ISO 27001 Annex A.12.4 (event logging): structured, parameterised, tamper-evident at the database level.
  • Industry-specific retention: rows are retained per the tenant's retention policy; deletion of a tenant cascades the log via company_id ON DELETE CASCADE — except for gdpr_operations, which is permanent.
Every action recorded. By the database, not by the application. The audit trail isn't a feature you opt into — it's the only way state can change.