TinyStore

Records / Events

Events

An event is a named fact a program appends: a role changed, a user signed up, a page was viewed. Unlike a log line, append waits until the event is written and loses none. Events live in named streams, each keeping its events as long as it says.

const audit = store.events('audit', { keep: '365d' })

await audit.append('role.changed', { user: 42, role: 'admin' })
await audit.append('role.changed', { user: 42, role: 'admin' }, { at: changedAt, context: { actor: admin.id } })
await clicks.append(batch.map(click => ({ name: click.type, fields: click.props, at: click.time }))) // all or none
audit = store.events("audit", keep="365d")

await audit.append("role.changed", {"user": 42, "role": "admin"})
await audit.append("role.changed", {"user": 42, "role": "admin"}, at=changed_at, context={"actor": admin.id})
await clicks.append([Event(name=click.type, fields=click.props, at=click.time) for click in batch])
audit, err := records.OpenEvents(store, "audit", records.Keep(365*24*time.Hour))

err = audit.Append(ctx, "role.changed", records.Fields{"user": 42, "role": "admin"})
err = audit.Append(ctx, "role.changed", change, records.At(changedAt), records.Context(records.Fields{"actor": admin.ID}))
err = clicks.AppendMany(ctx, events) // []records.Event{{Name, Fields, At}}, all or none
let audit = store.events("audit").keep(Duration::from_hours(365 * 24)).open()?;

audit.append("role.changed", &RoleChanged { user: 42, role: "admin" })?;
audit.event("role.changed").at(changed_at).context(&Actor { actor: admin.id }).append(&change)?;
clicks.append_all(batch)?; // NewEvent::new(name).fields(&props).at(time), all or none
  • append returns once the event is written, and a read finds it at once. A crash of the program loses none. A power loss may take the last ones, since the events' file commits as os unless the store opened full.
  • Appends from many callers share a commit.
  • A list is written whole or not at all, and a refused event names its place in the list and its name.
  • A stream begins with its first event; nothing declares it. Its name is [a-z0-9][a-z0-9_-]{0,63}.

What an event carries

FieldMeaning
atits time; kept to the nanosecond, and events are read in its order
streamthe stream it was written to
namewhat happened, role.changed; log for a logger's line
leveltrace, debug, info, warn, error or fatal; absent on an event without one
messagea line's text; absent on an event without one
fieldswhat happened: any object JSON writes
contextwhat it happened in: who asked, which request
traceId, spanId32 and 16 hex digits, as a traceparent header writes them

A value comes back as it was spelled: 1.2300, -0, the order of the keys come back byte for byte. In TypeScript an integer past 2⁵³ reads as a bigint.

append hides nothing: redact is a logger's. Leave out what may hold a secret before appending.

Time

at is now unless given. It is taken from as long ago as the stream keeps, to ten minutes ahead of the store's clock:

events audit: event 0 "x": more than 10 minutes ahead of the store's clock: too new

Events from clocks you do not own, a phone's, a browser's, are appended with the time they were received, and keep their own time as a field:

await clicks.append('view', { page, clientAt: body.at }) // at: now, as the server saw it
await clicks.append("view", {"page": page, "client_at": body.at})
err = clicks.Append(ctx, "view", records.Fields{"page": page, "clientAt": body.At})
clicks.append("view", &View { page, client_at: body.at })?;

A phone set a year back cannot bury its events in last year's segments, and a phone set ahead cannot write the future.

How long a stream keeps

keep is the stream's own, kept in the file with it: a logger and an events of one name share it, and so do two runs of a program.

  • A stream nobody gave a keep keeps 14 days.
  • An open that gives one sets it. While the store is open, giving another is invalid, naming both, so one part of a program never shortens what another keeps:
events audit: keep 30d, where the stream is open with keep 365d: invalid
  • An event older than its stream keeps is never read, whether or not it has left the file yet. Expiry removes whole segments, so there is no cleanup job and no delete that rewrites rows.

Logger or append?

You wantUse
lines a developer reads, never slowing a requestthe logger
a fact that must not be lost: audit, billingappend
analytics a program can afford to lose under loadlog.event(name, fields)

All three write the same events to the same streams, and every read finds them all.

Limits

WhatLimit
an event256 KiB, and 128 keys in all
one append4 MiB
an event's timefrom keep ago to ten minutes ahead
keep14 days unless the stream says

See also

  • Records: the engine's overview.
  • Reading: finding, counting and aggregating events.

Edit this page on GitHub Updated 19 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results