Inkress ADRsarchitecture decisions
Overview / Activity feed
ADR 0047

The merchant activity feed is a read-time merge of source tables, not an event store

AcceptedActivity feed

Context

The mobile app needs a merchant activity feed — one reverse-chronological stream of "what happened": orders created/paid/failed, payouts sent/failed, disputes opened, verification changed. The obvious way to build a feed is to write an event to a dedicated feed/event table on every such action; that, however, means touching the write path of every one of those domains and maintaining a new store. The existing audit trail (ADR 0037) is also not a reliable source, because its hook does not fire on the standard update path.

Decision

Assemble the activity feed at read time by merging the existing source tables, with no event table and no changes to any domain's write path.

  • Read-time merge with a keyset cursor. GET /api/v1/feed merges commerce events from their source tables (orders, ledger/payouts, disputes, verification) into one reverse-chronological stream, paginated by an opaque keyset cursor. Each entry's data carries the concrete display values the client needs (e.g. customer_name, the captured total from the captured ledger transaction, item_count, a pre-masked payout destination_label), every field nullable so a client can still render when a value is missing.
  • State-based derivation, not audit events ("Variant B"). Paid/failed entries are derived from orders.status + status_on, not from audit_logs order_status_changed rows — because that audit hook does not fire on the standard order-update path (a verified gap, see ADR 0037's append-only-by-convention weakness).

Consequences

  • Positive: the feed shipped with zero write-path changes across the contributing domains and no new event store to operate; it reads the same source-of-truth rows the rest of the platform uses, so it can't drift from them.
  • Neutral / to revisit: because entries are derived from current mutable state, the feed shows the latest state, not a true event history — an order whose status changed twice appears once, and ordering leans on status_on timestamps. A genuine event history would need the event store this decision avoided.
  • Neutral / to revisit: the merge must know where each field lives (e.g. the captured total is a specific ledger transaction, distinct from the order's own total, ADR 0034), so the read query is coupled to several tables' shapes; and it exists partly to route around the unreliable audit hook, which is worth fixing at the source (ADR 0037).

Alternatives considered

  • An event-sourced feed/event table written on every action: rejected — it requires changing the write path of every contributing domain and standing up a new store to keep in sync; a read-time merge needs neither and cannot fall out of step with the source rows.
  • Use the audit trail (audit_logs) as the feed source: rejected — the audit hook does not fire on the standard update path (ADR 0037), so an audit-sourced feed would silently miss events; deriving from orders.status is reliable today.
← 0046 Notifications are medium-tagged records fanned to per-channel adapters via a polling outbox