# 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](/docs/declarations) 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](/docs/the-substrate-ladder) names those lines precisely, because a platform that pretends to be everything ends up being nothing well.
