Skip to content

Timeline

Try the interactive example ↓

Append-only event streams with time-series analytics: named streams with retention policies and event caps, single and batch appends, time-range queries with filters and cursor pagination, bucketed counts, numeric field aggregation, structured search, and real-time WebSocket follow.

When to reach for it: activity feeds, audit trails, user behavior tracking, order and session history - any "what happened, and when" record you want to query by time window and tail in real time.

When not to: operational counters and gauges belong to Metrics; work that must be processed and acknowledged belongs to Job Queue; ephemeral broadcast with no history belongs to Pub/Sub.

Concepts

  • Streams are append-only logs - events are immutable once written. There is no update or delete of an individual event; you delete the whole stream, or let retention prune old events.
  • Events carry a type (dot-namespaced by convention, like user.login), an arbitrary JSON data payload, and optional string tags. The server assigns an id and a per-stream sequence number.
  • Timestamps are Unix epoch milliseconds everywhere - event timestamps, query ranges, cursors.
  • Streams auto-create on first append. create-stream exists to set description, retention, an event cap, and tags up front - not as a precondition.
  • Streams are shared across the tenant - any authenticated user can list, append to, and read any stream (verified with a second user's token). There is no per-user ownership.
  • Four read tools - query (events in a window, oldest first or with --sort -timestamp newest first), count (buckets per interval), aggregate (one number per field), search (structured filters). They have their own page: Queries and analytics.

Streams

snug timeline create-stream -n docs4-activity -d "Docs demo stream" -r 30 -t env:docs
snug timeline list-streams
snug timeline stream-stats -n docs4-activity
snug timeline delete-stream -n docs4-activity

Stream commands take -n; event and query commands take -s. Stats show the extent of what is stored - captured live:

{
  "stream_name": "docs4-activity",
  "event_count": 5,
  "oldest_event": 1787893274528,
  "newest_event": 1787893308689,
  "size_bytes": 0
}

stream-stats and delete-stream on an unknown stream fail with 404 stream_not_found. Reads and writes do not: querying a stream that does not exist returns an empty result, and appending to one silently creates it.

Appending events

snug timeline create-event -s docs4-activity -t user.login \
  -d '{"user_id":"u123","amount":25}' --tags region:us
snug timeline batch-create -s docs4-activity -f events.json

batch-create reads a bare JSON array; each item takes type and data plus optional timestamp and tags (the stream comes from -s):

[
  { "type": "order.placed", "data": { "order_id": "o-1", "amount": 30 } },
  { "type": "order.placed", "data": { "order_id": "o-2", "amount": 60 }, "tags": ["priority"] }
]

Over HTTP a single append is POST /api/v1/timeline/events:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stream":"docs4-activity","type":"user.login","data":{"user_id":"u123"}}' \
  http://localhost:4000/api/v1/timeline/events
{
  "status": 201,
  "msg": "Created",
  "data": {
    "event_id": "evt_WNcSLbZnWBPh",
    "stream": "docs4-activity",
    "timestamp": 1787893388879,
    "sequence": 6
  }
}

--timestamp backfills an event at a chosen time (epoch milliseconds). Timestamps may run at most one hour ahead of server time; beyond that the append fails with 400 invalid_timestamp - verified with a +2h timestamp.

Reading it back

snug timeline query -s docs4-activity --sort -timestamp --limit 10
snug timeline query -s docs4-activity --filter 'timestamp[gte]=-1h' --filter 'type[prefix]=user.' --limit 50

Filters on type, tags and event data, cursor paging, counts and aggregation are on the Queries and analytics page.

Following a stream in real time

follow opens a WebSocket on GET /api/v1/timeline/streams/{stream}/follow and prints every event appended from that moment until interrupted. Captured live, with an event appended from a second shell (--raw shows frames verbatim):

snug timeline follow -s docs4-activity --raw
{"type":"event","event":{"id":"evt_BdQKegubvFmT","stream":"docs4-activity","type":"follow.test","timestamp":1787893400766,"sequence":0,"data":{"n":1}}}

One quirk: sequence in pushed frames is 0, not the stored sequence number (the same event read back with query had sequence: 7) - identify followed events by id. Browser clients can pass the JWT as a ?token= query parameter instead of the Authorization header. The full message protocol is in the Timeline API reference.

Limits and configuration

Events up to 64 KB, batches up to 100 events per request, 100 streams, retention up to 365 days (30 by default), and 1000 results per query - tunable, along with the automatic cleanup worker, via the TIMELINE_* variables (TIMELINE_MAX_BATCH_SIZE can only lower the 100-event cap, which request validation enforces first).

Reference

Interactive example

Record an ordered history of events that can be queried by time. Earlier entries stay intact as new activity is appended.

An illustrative example that runs in your browser. No account or API calls required.