OriginChain docs
examples · atomic · 4 / 5

4. Audit event (row + FTS)

← Atomic multi-shape
what this does

The simplest multi-shape recipe. One event row in audit.events plus a BM25 full-text index over the human-readable payload. No vector, no graph - just structured event + searchable text.

when to use it
  • Compliance / SOC 2 audit logs you'll search by free text later ("who changed the carbon_plate column?").
  • Activity feeds where the structured fields (actor_id, action, ts_ms) are the filter and the payload text is what humans read.
  • Application logs where you want both fast time-range scans and grep-style search over the message.
the schema

Two indexes: one for "find all events by this actor in this window" and one for global time-range queries.

# audit/events.toml
namespace   = "audit"
table       = "events"
primary_key = ["id"]

[[columns]]
name = "id"
ty   = "str"
required = true

[[columns]]
name = "actor_id"
ty   = "str"
required = true

[[columns]]
name = "action"
ty   = "str"
required = true

[[columns]]
name = "payload_text"
ty   = "str"

[[columns]]
name = "ts_ms"
ty   = "u64"
required = true

[[indexes]]
name    = "by_actor_and_time"
columns = ["actor_id", "ts_ms"]

[[indexes]]
name    = "by_time"
columns = ["ts_ms"]
call 1 of 2 - the event row
POST /v1/tenants/:t/rows/audit.events
curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/rows/audit.events" \
  -H "Authorization: Bearer $OC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id":           "evt-2026-06-10-00042",
    "actor_id":     "user.alice",
    "action":       "schema.update",
    "payload_text": "Added column carbon_plate to shop.products",
    "ts_ms":        1749500000000
  }'
call 2 of 2 - the keyword index

Index the sanitized payload text. Same doc_id as the row's primary key so search hits join back to the event.

POST /v1/tenants/:t/fts/audit.events/index
curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/fts/audit.events/index" \
  -H "Authorization: Bearer $OC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "field":  "payload_text",
    "doc_id": "evt-2026-06-10-00042",
    "text":   "Added column carbon_plate to shop.products"
  }'
about atomicity

The row write and the FTS index are separate calls. There is no single "write everything" endpoint. Each is atomic by itself. The SDKs auto-attach an Idempotency-Key, so if the FTS index fails after the row succeeded, you can safely retry just the FTS call - re-doing the row write would not duplicate the event.

common mistakes
  • Indexing PII without redaction. Anything that lands in the FTS index is searchable forever. Sanitize emails, phone numbers, tokens, internal IDs before calling db.fts.index - never after.
  • Mutating audit events. Audit logs should be append-only. Don't re-put the row to "correct" an event - emit a new event that supersedes it.
  • Embedding raw JSON in payload_text. FTS tokenization treats and " as noise. Render the payload as a human sentence first so the index actually has searchable words.
  • Forgetting an id. If the row write generates an id server-side and the FTS call uses a different one, search hits won't join back. Set the id client-side (UUID or a sortable like ULID) and use the same string for both calls.