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/feedmerges commerce events from their source tables (orders, ledger/payouts, disputes, verification) into one reverse-chronological stream, paginated by an opaque keyset cursor. Each entry'sdatacarries the concrete display values the client needs (e.g.customer_name, the captured total from the captured ledger transaction,item_count, a pre-masked payoutdestination_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 fromaudit_logsorder_status_changedrows — 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_ontimestamps. 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 fromorders.statusis reliable today.