Skip to content
Browse docs

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.

When an activity event identifies the same entity and version as a persisted snapshot, Activity shows them together in one row, including the actor, reason, changed fields, and version. Comments and separate events remain visible. Matching version numbers on different entities do not combine their history.

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.

Creation, updates, and connections

The original creator belongs to the stable entity. Each revision also has its own creation time and author. Headers use the server’s lifecycle evidence to preserve original creation while showing Updated for a later revision, even if timestamp precision makes the two times equal. Missing historical evidence is shown as unknown; an unavailable display name does not erase the retained actor ID.

Entity history includes creation regardless of its original page tags. A connection row opens history for its stable relationship identity from either navigation direction, including after disconnect. Page and section activity use their matched governed scope. List snapshots provide separate comparison and supported restoration; their authors are not connection creators.

Opening history, retrying a failed request, and loading more events do not create audit events or versions. A failed request offers retry and retains previously loaded evidence. Load more keeps older creation evidence reachable.

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

Projection repair is dry-run by default and requires no new schema migration:

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.

New writes always persist scope projections within the mutation transaction. Explicit entity, relationship, and scope reads use these projections independently of the legacy activity cohort. AUDIT_SCOPE_SHADOW_COMPARE_ORG_IDS retains sampled comparison diagnostics for eligible first-page reads. Application rollback requires reverting the release; changing the old cohort flags does not disable the repaired explicit history modes. Keep immutable events and additive projections intact.