Skip to main content

Context

This is the feature that justifies the module, and it is the one thing in the suite that competitors structurally cannot copy. A loyalty tool sees points. An email platform sees sends. A CRM sees notes. Only a system where all three share a correlation_id (DEC-E7) can show the causal chain rather than three lists sorted by date. It is a projection, not a table. Materializing it would mean a fifth copy of data that already exists, kept in sync by a job that will drift. It merges live from the sources at read time. The permission trap is real and worth stating: a joined view is exactly where a permission gets forgotten. An agent without core.contacts.read_national_id must see a masked one here too, and an agent without loyalty.ledger.read must not learn balances through the timeline.

Scope (normative)

  • A merge-sorted read over: core.tracked_events, loyalty.ledger_transactions, messaging.sends with status, crm.notes and crm.activities.
  • Cursor pagination by occurred_at.
  • Filtering by source and by date range.
  • Chain rendering: entries sharing a correlation_id grouped as one causal sequence.
  • Permission filtering per source.

Non-scope (normative)

  • Materializing the timeline. It is computed at read time, always.
  • Cross-contact reporting. This is one contact’s history.
  • Editing anything from the timeline. It is a read surface; actions happen in their own module.
  • Real-time streaming — F1b if asked for.

Behaviour (normative)

  1. The timeline is computed at read time from its sources. There is no timeline table, no materialized view, and no sync job.
  2. Each source is queried with its partition-key predicate. The date range is therefore required, not defaulted, and the API says so — a timeline without one is a full scan of the largest tables in the platform.
  3. Entries are merge-sorted by occurred_at descending, with cursor pagination. Sources are queried in parallel and merged, never fetched fully and then sorted.
  4. Every source is permission-filtered independently. A reader without a source’s permission does not see its entries, and does not learn they exist from a gap or a count.
  5. Sensitive fields are masked according to the reader’s permissions, exactly as in the owning module (DEC-A4). The timeline never becomes the loophole.
  6. Entries sharing a correlation_id are grouped as one chain with their causal order preserved. That grouping is the feature; a flat list sorted by date is what every other tool already offers.
  7. Cross-module reads go through each module’s public read path, never its tables. The boundary holds even for a read (ADR-022’s discipline, applied here).
  8. An unavailable source degrades gracefully: the timeline renders the rest and says which source is missing. A messaging outage should not blank a support agent’s screen.

Data (normative)

No tables. This feature owns a projection and no storage — which is the whole point.

API (normative)

The single permission gates access to the timeline; each source is filtered by its own permission on top. Holding crm.timeline.read alone shows notes and activities and nothing else.

Events (normative)

None. A read surface emits nothing.

Acceptance criteria (normative)

  1. First page p95 <1 s for a contact with 5 000 events across all sources.
  2. A request without a date range is rejected with a typed error.
  3. Every source query carries its partition-key predicate — verified by capturing the queries.
  4. A reader without a source’s permission sees none of its entries and cannot infer them from counts or gaps.
  5. A national ID appears masked for a reader without core.contacts.read_national_id.
  6. Entries sharing a correlation_id render as one chain in causal order — the purchase → points → email → open → redemption case, end to end.
  7. A source returning an error degrades gracefully and names the missing source.
  8. Cursor pagination returns each entry exactly once across pages, with no duplicates at boundaries.
  9. Negative: no cross-module table is read directly, enforced by dependency-cruiser.

Execution

Single slice, synchronous command. Sources queried in parallel with a per-source timeout; merge and permission filtering in backend/api.

Open questions

Changelog

Delivery record

Not implemented yet.