# 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.

## Before you run it

[Section titled “Before you run it”](#before-you-run-it)

-   Complete [Installation](/docs/start/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.

Tutorial storage only

`MemoryAuditStore` exercises append, ordering, and verification behavior but loses everything when the process exits. Select an [authoritative durable store](/docs/storage/overview/) before production.

## Two records, one tenant-local chain

[Section titled “Two records, one tenant-local chain”](#two-records-one-tenant-local-chain)

src/examples/tutorial/record-and-verify.ts

```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

```sh
bun src/examples/tutorial/record-and-verify.ts
```

The checked fixture prints this exact output:

verified output

```json
{
  "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.

## What your application supplies

[Section titled “What your application supplies”](#what-your-application-supplies)

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.

## What the store adds

[Section titled “What the store adds”](#what-the-store-adds)

`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.

## Checkpoint: Verification passed

[Section titled “Checkpoint: Verification passed”](#checkpoint-verification-passed)

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](/docs/start/verify-a-chain/).

[Edit page](https://github.com/getveritio/veritio-website/edit/main/src/content/docs/docs/start/record-first-event.mdx)

Last updated: Aug 23, 2026

[Previous  
Installation](/docs/start/installation/)[Next  
Verify a chain](/docs/start/verify-a-chain/)

Veritio provides evidence support, not legal advice or automatic compliance.

This site uses cookieless, anonymous analytics (Umami) by default. With your consent, we also enable Google Analytics, which sets cookies and sends usage data to Google. [Privacy Policy](/legal/privacy/)
