Skip to content

Audit

How immutable activity, exact entity versions, and governed page scopes form a deterministic audit trail.

Why it matters

TrialStack separates two records that answer different questions:

  • Activity events record what happened, who acted, why, and which governed surface was affected.
  • Entity versions are persisted snapshots owned by one exact entity. Only these versions can be compared or restored.

An activity event is never treated as a version merely because it mentions the same version number.

How it works

Every new governed event carries a canonical subject, optional exact version reference, and scope:

root entity -> layout -> page -> affected sections

A single event may affect several sections. It appears once in page or layout activity and once in each affected section, with event-ID deduplication. Relationship events are labeled as context and cannot restore the parent.

Section headers keep the governed scope visible while you work. Changes is shown for every section, including zero. Comments opens Activity filtered to that exact section, and verification findings are shown only when unresolved findings exist.

Reviewing selected changes

Before a governed save, the review compares the current draft with the persisted baseline. You can select individual business-field changes; section-level selection controls are shortcuts for selecting or clearing their leaf fields.

TrialStack applies those selections at the form’s persistence boundary. Config-driven entity pages first project both the baseline and the draft into the canonical update payload, then retain only the selected paths. Direct forms with display-only comparison adapters apply the selection to their form data before building the save request. After a canonical selected save succeeds, the form refreshes from the server response so the visible values match the saved version.

If every change is deselected, Save remains unavailable. Omitting a selection preserves the normal full-save behavior.

The API derives organization and scope at the trusted persistence boundary. Activity reads use an immutable projection table and a stable (occurredAt, auditEventId) cursor. The projection can be rebuilt without changing audit events or their checksums.

When to use it

Use section and page activity to review operational changes and comments. Use exact entity history when comparing or restoring persisted state. Use layout or root activity to understand changes across owned pages and labeled relationships.

What to watch for

Legacy events are projected only when their scope is deterministic. Ambiguous events remain legacyUnscoped; they do not silently appear in every page.

Operations

The projection migration is dry-run by default:

pnpm --filter @trialstack/api db:backfill:audit-scope-projections
pnpm --filter @trialstack/api db:backfill:audit-scope-projections -- --org <organization-id>

Apply only after reviewing the per-organization counts:

pnpm --filter @trialstack/api db:backfill:audit-scope-projections:apply

The command is idempotent. It inserts unique projection rows and never updates immutable events or checksums. Re-running it is the supported rebuild procedure.

Rollout is controlled with AUDIT_SCOPE_PROJECTION_WRITE_ORG_IDS and AUDIT_SCOPE_ACTIVITY_ORG_IDS (* or comma-delimited organization IDs). AUDIT_SCOPE_SHADOW_COMPARE_ORG_IDS enables sampled legacy/canonical comparison warnings for selected organizations. To roll back reads, remove an organization from the activity cohort while leaving immutable events intact; projections can remain for later re-enablement.