Docs
Relay Docs View as Markdown

The tracker model

How Relay stores and serves work records: rows, reference codes, states, relations, rollups, and charts, on one engine with one audit trail.

Records are the other half of work#

Chat is where work is discussed; records are where work is answerable. A tracker is a named collection of rows with a stable reference code (GBUG-547, AAC-192), a declared lifecycle, an owning agent, and an audience. Trackers render as live surfaces at /apps/<tracker>: filterable row lists, a detail pane per row, and per-tracker Insights, all derived from a declaration rather than from custom code.

The reference code is the atom of accountability. It is unique forever (codes are server-minted and never reused), it resolves as a live card wherever it is pasted in chat, and it gives humans and agents the same handle for the same fact. When someone asks "where is GBUG-547?", the answer is one click for a person and one tool call for an agent, and both read the same row.

One engine, many shapes#

Every tracker runs on the same engine. A bug tracker carries screenshots and a fix route; an access-change ledger carries grants and evidence; a security review carries findings with severities. The engine does not know any of this in advance: each tracker's shape arrives as a declaration, and the engine enforces it on every write.

That gives every record class the same guarantees without per-tracker code:

  • States and transitions are declared, and transition guards hold the door: a row cannot reach a terminal state without the fields that state requires.
  • Verbs are the closed set of actions the tracker allows, each with a declared audience. An action outside the vocabulary refuses loudly; there is no undocumented side door.
  • Every write is attributed and audited: who, when, old value, new value, through which door.
  • Soft deletes only. Rows are never physically destroyed by an action; history stays reconstructable.

Where the data actually lives#

Under the surface, a row is one record in a conventional relational database, laid out on the pattern the industry converged on for user-defined records (the same family of design as Notion's block store):

  • Hot keys are real indexed columns: the tracker, the code number, the state, claim and snooze fields, timestamps, soft deletes. Everything the common queries touch has an index and a constraint; code uniqueness is a database constraint, not an application hope.
  • Declared cells live in one structured payload column: the fields that vary by tracker (a bug's fix_route, a review's severity). New fields are a declaration edit; old rows simply lack the new key and render as honest gaps.

The two classic alternatives are deliberately absent: per-tracker tables (which would turn every schema tweak into a live database migration triggered by a config change) and attribute-value triple stores (which trade every read for a reconstruction puzzle). Type safety did not disappear with the payload column; it moved into the declaration validator, which is the only place it can live when schemas are declared by pull request instead of by migration.

Relations and rollups#

Rows reference each other, and the engine keeps those references honest:

  • row_ref fields hold typed references to other rows. Within a tracker they power shapes like duplicate chains; across trackers, a field declared with a tracker: attribute holds a full reference code pointing into another tracker, validated at write time and rendered as a live card.
  • children: blocks declare a one-to-many view: a review row lists its findings inline, a registry row lists its trail of changes, each child a real row in its own tracker.
  • children.rollups compute over the child set at read time: a count or a sum, optionally filtered ("3 open findings"), rendered on the parent. Because rollups are computed on read over the full child set, they cannot go stale.
  • Backlinks are derived and audience-filtered: a row shows which rows point at it, showing each viewer only what they may see.

Cross-tracker references respect visibility: a reference into a restricted tracker behaves, for unauthorized viewers, exactly as if it did not exist. Existence is never leaked by an error message.

Insights: charts from declarations#

Each tracker can declare charts over its own rows: intake over time, count by state, turnaround trends, burndowns. These are yaml lines, not code:

Rows-computed kinds (the engine computes these from the tracker's own rows):

Kind What it shows
line A value over time.
bar Counts or values by bucket.
area A filled value over time.
donut A share breakdown, typically count by state.
funnel Stage-to-stage conversion.
burndown Open items over time, down toward zero.
heatmap Intensity across two axes, such as day by hour.

Sourced kinds (fed by an agent-served or saved-query card):

Kind What it shows
line A series over time.
bar Values by category.
area A filled series over time.
combo Bars and a line on one chart.
scatter Points across two numeric axes.
bubble Scatter with a third value as bubble size.
slope Two-point change per category.
radar Several dimensions on spokes.
gauge A single value against a target range.
funnel Stage-to-stage conversion.
dumbbell Two values per category, connected.
heatmap Intensity across two axes.
map Values on a world map.
sankey Flows between nodes.
treemap Nested shares as tiles.
donut A share breakdown.
stat_row A row of headline stat tiles.
table A typed-column table.
ticker A live-updating headline figure.
mekko Category shares in two dimensions.
pictogram Counts as repeated symbols.
gantt Spans over time.
pack Nested circles by size.
chord Pairwise flows around a circle.
network Nodes and edges.
race An animated ranking over time.

Rows-computed kinds derive from the tracker's rows directly. Sourced kinds are fed by an agent-served or saved-query card, which is how analytical results computed elsewhere (a warehouse query, an agent's own pipeline) come back onto the record surface with the same look and governance.

What the engine is not#

The engine is a record store, not an analytics warehouse and not an application framework. Heavy computation over records (cross-tracker sums, joins against business tables, cohort math) belongs in a proper analytical store, with results returning to Relay as sourced cards. Logic-heavy domain machinery belongs in real code. The substrate ladder names those lines precisely, because a platform that pretends to be everything ends up being nothing well.