Skip to content
VeritioDocs

Record your first audit event

Kind
tutorial
For
newcomer · developer
Verified against
@veritio/core@0.4.7

This tutorial records an invitation followed by the resulting membership. The two actions share one tenant scope, so the store orders them in one chain and the second record points to the first record’s hash.

  • Complete Installation for TypeScript.
  • Run this in a server-side module where the tenant and actor have already been authenticated.
  • Use the in-memory store only for this local exercise.
src/examples/tutorial/record-and-verify.ts
import {
type AuditRecord,
MemoryAuditStore,
createAuditEvent,
verifyAuditRecords,
} from '@veritio/core'
export const tutorialScope = { tenantId: 'org_acme', environment: 'production' } as const
/**
* Records the deterministic two-event chain reused by the opening tutorial and
* its tamper exercise. Fixed identifiers and timestamps keep the documented
* output reproducible while the store still assigns the authoritative sequence
* numbers, previous hashes, and record hashes.
*/
export async function recordTutorialChain(): Promise<AuditRecord[]> {
const store = new MemoryAuditStore()
const invitation = createAuditEvent({
id: 'evt_member_invited_01',
occurredAt: '2026-08-09T10:00:00.000Z',
actor: { type: 'user', id: 'usr_owner' },
action: 'organization.member.invited',
target: { type: 'organization', id: 'org_acme' },
scope: tutorialScope,
purpose: 'access_management',
lawfulBasis: 'contract',
retention: 'security_1y',
metadata: { role: 'viewer' },
})
const membership = createAuditEvent({
id: 'evt_member_joined_01',
occurredAt: '2026-08-09T10:01:00.000Z',
actor: { type: 'user', id: 'usr_member' },
action: 'organization.member.joined',
target: { type: 'organization', id: 'org_acme' },
scope: tutorialScope,
purpose: 'access_management',
lawfulBasis: 'contract',
retention: 'security_1y',
metadata: { role: 'viewer' },
})
await store.append(invitation, { idempotencyKey: 'invitation:inv_123' })
await store.append(membership, { idempotencyKey: 'membership:mem_123' })
return store.list(tutorialScope)
}
if (import.meta.main) {
const records = await recordTutorialChain()
const output = {
sequences: records.map((record) => record.sequence),
previousHashLinked: records[1]?.previousHash === records[0]?.hash,
verification: verifyAuditRecords(records),
}
console.log(JSON.stringify(output, null, 2))
}

Run it with:

Terminal window
bun src/examples/tutorial/record-and-verify.ts

The checked fixture prints this exact output:

verified output
{
"sequences": [
1,
2
],
"previousHashLinked": true,
"verification": {
"ok": true
}
}

sequences confirms gapless tenant-local ordering. previousHashLinked confirms that record two stores record one’s hash. verification.ok confirms that the current canonical bytes, sequence values, and links agree.

The host supplies the event identity, timestamp, actor, action, target, scope, governance labels, and minimized metadata. Resolve tenantId and actor identity from trusted server context; do not copy either from an untrusted request body without authorization.

The two idempotency keys identify the logical invitation and membership operations. A retry should reuse the same key and the same event bytes. A different operation needs a different key.

append creates the AuditRecord envelope: tenant-local sequence, previousHash, hash, appendedAt, the canonicalization label, and a hashed idempotency key. list requires tenant scope again before returning the ordered records.

That separation matters: the application describes what happened; the authoritative store owns ordering and conflict checks.

Before continuing, confirm all three output properties match the checked output above. A successful chain proves that the recorded bytes remain internally consistent. It does not prove the original application claim was truthful or authorized.

Next, tamper with the copied records and inspect the exact failures.