ServicesTimeline

Timeline

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.
  • Five read tools - query (events in a window), latest (tail), 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 latest -s docs4-activity -l 10
snug timeline query -s docs4-activity --start=-1h --end now -t 'user.*' -l 50

Write negative relative range values in = form: --start=-1h. The space form --start -1h is rejected by the argument parser, which reads -1h as a flag. Filters, wildcards, pagination, counts, aggregation, and structured search 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, reproduced live: sequence in pushed frames is 0, not the stored sequence number (the same event read back with latest 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 1000 events, 100 streams, retention up to 365 days (30 by default), and 1000 results per query - all tunable, along with the automatic cleanup worker, via the TIMELINE_* variables in the Timeline CONFIG reference.

Reference

On this page