Skip to content

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.

SeamCarriesSource of truth
Kafka events / commands (nx.bana.evt.* · nx.bana.cmd.*)cross-service application eventspackages/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 webhookstwo 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:

TopicTypeProducerConsumer(s)
nx.bana.evt.payment.succeededevtsalefinance, inventory, invoice
nx.bana.evt.inventory.purchase-order.receivedevtinventoryfinance
nx.bana.evt.inventory.stock.issued-for-saleevtinventoryfinance
nx.bana.evt.inventory.stock.adjustedevtinventoryfinance
nx.bana.evt.kitchen.ticket-item.status-changedevtsaleinventory
nx.bana.evt.inventory.material.transferredevtinventoryinventory (cross-location)
nx.bana.evt.inventory.material.stock-changedevtinventory(no consumer yet)
nx.bana.evt.signal.activity.notifiedevt(no producer yet)signal → WebSocket
nx.bana.cmd.ledger.generatecmdledgerledger (internal queue → worker)

KafkaTopics.* constants resolve to these names. signal.activity.notified has a consumer but no in-repo producer yet; material.stock-changed is produced but currently unconsumed - both are wired ahead of upcoming work. cmd.ledger.generate is ledger-internal (queue→worker decoupling), not a cross-service seam.

Consumer groups

ServiceSubscribes to
financepayment.succeeded, inventory.purchase-order.received, inventory.stock.issued-for-sale, inventory.stock.adjusted, CDC public.Merchant
inventorypayment.succeeded, kitchen.ticket-item.status-changed, inventory.material.transferred, CDC public.Merchant, CDC public.ProductVariant
invoicepayment.succeeded, CDC public.Merchant
signalsignal.activity.notified
searchall 27 CDC topics
ledgercmd.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:

ConsumerCDC topic(s)Projection
searchall 27 (SearchCollections.ALL_CDC_TOPICS)Typesense collections (products, merchants, categories, organizers, devices, sale-channels)
invoicenx.bana.cdc.public.MerchantTaxInfo (authoritative merchant tax profile)
financenx.bana.cdc.public.Merchantdefault Cash wallet on new merchant
inventorynx.bana.cdc.public.Merchant, …public.ProductVariantInventoryItem 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):

TopicPurpose
ws:observation.sale.sale-orderOrder created/updated
ws:observation.sale.sale-order-itemOrder item changes
ws:observation.sale.sale-checkCheck state changes
ws:observation.sale.kitchen-ticketKitchen ticket dispatched
ws:observation.sale.kitchen-ticket-itemKitchen ticket item updates
ws:observation.allocation.allocation-usageSeat / allocation usage changes

Payment (packages/payment/src/common/websocket.ts):

TopicPurpose
ws:observation.payment.transactionPayment transaction status updates
ws:observation.payment.payment-attemptIndividual payment attempt events

Outreach (packages/outreach/src/components/websocket/topics.ts):

TopicPurpose
ws:observation.outreach.inquiry.submittedNew 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 typePurpose
mq-pay:attempt.successPayment attempt succeeded
mq-pay:attempt.failedPayment attempt failed
mq-pay:attempt.expiredPayment attempt timed out
mq-pay:attempt.cancelledPayment attempt cancelled by user
mq-pay:transaction.settledFull transaction settled (all items paid)
mq-pay:transaction.cancelledTransaction 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)

PageDescription
ArchitectureService registry, dependency chain, component matrix
Platform FactsMessaging seam summary, canonical counts
Infrastructure - KafkaKafka KRaft cluster setup
Infrastructure - CDCDebezium CDC pipeline configuration

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.