Event Architecture
BANA services are decoupled through four seams. Redis is not one of them - it is cache, WebSocket cross-instance fan-out and BullMQ storage only.
| Seam | Carries | Source of truth |
|---|---|---|
Kafka events / commands (nx.bana.evt.* · nx.bana.cmd.*) | cross-service application events | packages/core/src/common/kafka/registry.ts (ServiceTopicDefs) |
Debezium CDC (nx.bana.cdc.<schema>.<Table>) | row changes → read-models (Typesense, projections) | same registry (CDC_TABLE_SCHEMA, CDCTopicDefs) |
| HTTP webhooks | two service-to-service hops (payment→sale, commerce→invoice) | per-service webhook config |
WebSocket (ws:…) | real-time push to clients (Redis-backed cross-instance fan-out) | per-service websocket.ts |
Legacy / dead code - not the messaging backbone
@nx/core's IEventBus + RedisPubSubAdapter and the PaymentEventChannels / CommerceEventChannels string constants are never instantiated for cross-service eventing. The real payment seam is the Kafka topic nx.bana.evt.payment.succeeded, not the string payment.order.success. Redis pub/sub moves WebSocket frames between replicas - it does not move application events between services.
Kafka events & commands
Topic naming: nx.bana.<evt|cmd>.<domain>[.<entity>].<event> - lowercase, .-separated, - for multi-word, past tense for evt, imperative for cmd. The ServiceTopicDefs registry is the complete list:
| Topic | Type | Producer | Consumer(s) |
|---|---|---|---|
nx.bana.evt.payment.succeeded | evt | sale | finance, inventory, invoice |
nx.bana.evt.inventory.purchase-order.received | evt | inventory | finance |
nx.bana.evt.inventory.stock.issued-for-sale | evt | inventory | finance |
nx.bana.evt.inventory.stock.adjusted | evt | inventory | finance |
nx.bana.evt.kitchen.ticket-item.status-changed | evt | sale | inventory |
nx.bana.evt.inventory.material.transferred | evt | inventory | inventory (cross-location) |
nx.bana.evt.inventory.material.stock-changed | evt | inventory | (no consumer yet) |
nx.bana.evt.signal.activity.notified | evt | (no producer yet) | signal → WebSocket |
nx.bana.cmd.ledger.generate | cmd | ledger | ledger (internal queue → worker) |
KafkaTopics.*constants resolve to these names.signal.activity.notifiedhas a consumer but no in-repo producer yet;material.stock-changedis produced but currently unconsumed - both are wired ahead of upcoming work.cmd.ledger.generateis ledger-internal (queue→worker decoupling), not a cross-service seam.
Consumer groups
| Service | Subscribes to |
|---|---|
| finance | payment.succeeded, inventory.purchase-order.received, inventory.stock.issued-for-sale, inventory.stock.adjusted, CDC public.Merchant |
| inventory | payment.succeeded, kitchen.ticket-item.status-changed, inventory.material.transferred, CDC public.Merchant, CDC public.ProductVariant |
| invoice | payment.succeeded, CDC public.Merchant |
| signal | signal.activity.notified |
| search | all 27 CDC topics |
| ledger | cmd.ledger.generate (internal) |
Debezium CDC
The Debezium connector topic.prefix = nx.bana.cdc emits nx.bana.cdc.<schema>.<Table> verbatim per Postgres table (CDC_TABLE_SCHEMA, kept in sync with the connector table.include.list). 27 tables are captured and consumed into read-models:
| Consumer | CDC topic(s) | Projection |
|---|---|---|
| search | all 27 (SearchCollections.ALL_CDC_TOPICS) | Typesense collections (products, merchants, categories, organizers, devices, sale-channels) |
| invoice | nx.bana.cdc.public.Merchant | TaxInfo (authoritative merchant tax profile) |
| finance | nx.bana.cdc.public.Merchant | default Cash wallet on new merchant |
| inventory | nx.bana.cdc.public.Merchant, …public.ProductVariant | InventoryItem seed |
Captured tables by schema: public (Organizer, Merchant, Category, Device, SaleChannel, Product, ProductInfo, ProductVariant, ProductCategory, ProductIdentifier, MetaLink, ProductBundler, ProductOption, ProductOptionValue, ProductVariantOption), pricing (FareSet, Fare), sale (SaleOrder), inventory (InventoryStock, InventoryItem, InventoryLocation, InventoryIdentifier, Material), identity (User, UserProfile, UserIdentifier, PolicyDefinition).
Connector-internal topics (not application data):
nx_bana_connect_configs/_offsets/_statuses,__debezium-heartbeat.nx.bana.cdc,nx.bana.cdc.public.debezium_signal, and the DLQ topics.
WebSocket topics
WebSocket topics follow ws:{namespace}.{domain}.{entity}; rooms follow wr:{namespace}/{path}. Built by WebSocketTopics / WebSocketRooms in packages/core/src/common/events/websocket-events.ts. Cross-instance delivery is fanned out over Redis (signal, sale, payment, helpdesk, outreach bind a Redis emitter adapter).
Sale (packages/sale/src/common/websocket.ts):
| Topic | Purpose |
|---|---|
ws:observation.sale.sale-order | Order created/updated |
ws:observation.sale.sale-order-item | Order item changes |
ws:observation.sale.sale-check | Check state changes |
ws:observation.sale.kitchen-ticket | Kitchen ticket dispatched |
ws:observation.sale.kitchen-ticket-item | Kitchen ticket item updates |
ws:observation.allocation.allocation-usage | Seat / allocation usage changes |
Payment (packages/payment/src/common/websocket.ts):
| Topic | Purpose |
|---|---|
ws:observation.payment.transaction | Payment transaction status updates |
ws:observation.payment.payment-attempt | Individual payment attempt events |
Outreach (packages/outreach/src/components/websocket/topics.ts):
| Topic | Purpose |
|---|---|
ws:observation.outreach.inquiry.submitted | New inquiry submitted (real-time admin notification) |
HTTP webhooks
payment → sale
The sale service receives payment events from MQ-Pay over HTTP, dispatched on the X-Webhook-Event-Type header (packages/sale/src/common/webhook-types.ts):
| Event type | Purpose |
|---|---|
mq-pay:attempt.success | Payment attempt succeeded |
mq-pay:attempt.failed | Payment attempt failed |
mq-pay:attempt.expired | Payment attempt timed out |
mq-pay:attempt.cancelled | Payment attempt cancelled by user |
mq-pay:transaction.settled | Full transaction settled (all items paid) |
mq-pay:transaction.cancelled | Transaction cancelled |
On a settled order the sale service then produces the Kafka topic nx.bana.evt.payment.succeeded. Inside the MQ-Pay library these are first emitted on a Node.js EventEmitter (mq-pay:transaction.*, mq-pay:attempt.*, mq-pay:refund.*) and bridged to sale via MQPaySaleEventAdapter → SaleEventMapperService → SalePaymentEventHandlerService.
commerce → invoice
commerce dispatches commerce:organizer.hq_changed as an HTTP webhook (WebhookDispatcherService + WebhookConfig); invoice consumes it via CommerceWebhookService.handleEvent to refresh issuance profiles.
Flows
New merchant onboarding (CDC-driven)
Payment
Product change → read-models (CDC)
Related Pages
| Page | Description |
|---|---|
| Architecture | Service registry, dependency chain, component matrix |
| Platform Facts | Messaging seam summary, canonical counts |
| Infrastructure - Kafka | Kafka KRaft cluster setup |
| Infrastructure - CDC | Debezium CDC pipeline configuration |