Audit Trail on Tawa

The Short Version

import { Septor } from '@insureco/septor'

const septor = new Septor({
  apiUrl: process.env.SEPTOR_URL,
  namespace: process.env.SERVICE_NAME || 'my-service',
})

// Fire-and-forget — NEVER await septor on the critical path
septor.emit('payment.created', {
  entityId: orgSlug,
  data: { amount, referenceId: paymentId },
  metadata: { who: userId },
}).catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))

Setup

Declare septor as an internalDependency to get SEPTOR_URL auto-injected:

spec:
  internalDependencies:
    - service: septor
      port: 3000    # creates SEPTOR_URL env var

Fire-and-Forget (REQUIRED)

A Septor outage must NEVER break your service. Always fire-and-forget:

// ✅ CORRECT
septor.emit('payment.created', { ... })
  .catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))

// ❌ WRONG: blocking your user on an audit write
await septor.emit('payment.created', { ... })

What MUST Be Septor-Wired

CategoryRequired Events
Paymentspayment.created, payment.completed, payment.failed, payment.refunded
Policiespolicy.bound, policy.endorsed, policy.cancelled, policy.renewed
Complianceofac.cleared, ofac.flagged, kyc.verified, kyc.failed
Authorizationuser.login, permission.granted, permission.revoked
Data Accessrecord.accessed, report.exported, data.modified

Automatically written by the platform (no code needed):

  • Deploy events (builder writes these)
  • Gas events (Janus writes these)
  • Job/cron events (iec-queue/iec-cron write these)
  • Credential rotations (builder writes these)

Event Naming

Use {resource}.{action} dot notation, lowercase:

payment.created       ✅
policy.bound          ✅
ofac.screen.cleared   ✅  (nested resource.sub.action is OK)

payment-created       ❌  (hyphens — use dots)
createPayment         ❌  (camelCase)
PAYMENT_CREATED       ❌  (uppercase)

Querying the Audit Trail

const { data } = await septor.query({
  entityId: orgSlug,
  eventType: 'payment.created',  // optional filter
  from: '2024-01-01',
  to: '2024-01-31',
  limit: '100',
})

for (const event of data.events) {
  console.log(event.eventType, event.createdAt, event.metadata.who)
}

Verifying Chain Integrity

// Run in background jobs, not request handlers — this is O(n)
const { data } = await septor.verify(orgSlug)
if (!data.valid) {
  logger.error({ brokenAt: data.brokenAt }, 'Audit chain broken!')
}

Key Facts

  • Each event gets a cryptographic hash linked to the previous — tamper-evident chain
  • entityId is the primary index — use a stable identifier (orgSlug or userId)
  • Events are immutable — once written, cannot be modified or deleted
  • Each service sees only its own events (namespace scoping)
  • Missing .catch() is a compliance gap — failures must be logged

What NOT to Do

// ❌ WRONG: blocking on audit write
await septor.emit(...)

// ❌ WRONG: silent failure
septor.emit(...).catch(() => {})  // swallows the error — compliance gap

// ❌ WRONG: vague event type
septor.emit('event.happened', ...)

// ✅ CORRECT
septor.emit('payment.refunded', {
  entityId: orgSlug,
  data: { amount, reason, refundId },
  metadata: { who: userId, why: 'Customer requested refund' },
}).catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))

Last updated: February 28, 2026