Addresses 11 original findings (1 critical, 3 high, 4 medium, 3 low) plus 4 additional findings from security review: - Mask secrets in PUT /settings response (was leaking decrypted keys) - Add paymentCheckoutRateLimit (10/hr/IP) to all 5 checkout endpoints - Implement durable audit logging to payment_audit_log table - Pin Stripe API version to 2026-01-28.clover (SDK v20.3.1) - Add charge.dispute.created/closed webhook handlers with DISPUTED status - Restore tickets on dispute won, handle charge_refunded closure - Guard against sentinel passthrough corrupting stored Stripe keys - Wrap refund DB updates in try/catch with webhook reconciliation fallback - Add $transaction for product maxPurchases race condition - Remove dead Payment model lookup from handleChargeRefunded - Cap donation amount at $100k in both schemas - Add requirePaymentsEnabled middleware on all checkout routes - Remove Stripe internal IDs from CSV exports - Add Cache-Control: no-store on admin settings responses Bunker Admin
6.3 KiB
6.3 KiB
Service Integrations — EventBus Architecture
Tracking document for the platform-wide EventBus and service integration work.
Started: 2026-03-30 Branch: v2
Architecture Overview
Changemaker Lite has 30+ services but most operate as isolated tools. The EventBus provides a centralized, typed, in-process pub/sub system that decouples event producers from consumers.
Service Handler (shift created, donation completed, etc.)
|
v
eventBus.publish('shift.created', payload)
|
+-- ListmonkListener (newsletter sync)
+-- RocketChatListener (team notifications)
+-- CrmActivityListener (contact timeline)
+-- CalendarSyncListener (unified calendar)
+-- N8nWebhookListener (external automation)
+-- GancioSyncListener (public event calendar)
Why In-Process EventEmitter (not Redis PubSub)
- Single Express process — no distributed coordination needed
- Zero serialization overhead (pass JS objects directly)
- Data already persisted in DB — events are ephemeral notifications
- Matches the existing fire-and-forget pattern used by Listmonk/RC services
- Can be swapped to Redis PubSub later if we go multi-process
Key Files
| File | Purpose |
|---|---|
api/src/types/events.ts |
Typed event catalog (all event names + payloads) |
api/src/services/event-bus.service.ts |
Core EventBus (publish/subscribe/stats) |
api/src/services/event-listeners/listmonk.listener.ts |
Listmonk newsletter sync |
api/src/services/event-listeners/rocketchat.listener.ts |
Rocket.Chat notifications |
api/src/services/event-listeners/crm-activity.listener.ts |
CRM ContactActivity writer |
api/src/services/event-listeners/calendar-sync.listener.ts |
Calendar unification |
api/src/services/event-listeners/n8n-webhook.listener.ts |
n8n automation bridge |
api/src/services/event-listeners/gancio.listener.ts |
Gancio event sync |
Progress Tracker
Phase 1: Core Infrastructure
- Explore existing event patterns (Listmonk, RC, Gancio, provisioning)
- Design EventBus architecture
- Implement EventBus service (
api/src/services/event-bus.service.ts) - Define typed event catalog (
api/src/types/events.ts— 46 events across 14 modules) - Register EventBus in server.ts startup
- Add EventBus stats endpoint (
GET /api/observability/event-bus)
Phase 2: Migrate Existing Integrations
- Listmonk event sync → EventBus listener (9 event subscriptions)
- Rocket.Chat webhook service → EventBus listener (4 event subscriptions)
- Gancio shift/event sync → EventBus listener (3 event subscriptions)
Phase 3: New Listeners
- CRM Activity auto-generation listener (11 event subscriptions)
- Calendar sync listener (8 event subscriptions)
- n8n webhook emitter listener (wildcard subscription, forwards all events)
- Listmonk webhook receiver (inbound: open, click, bounce, unsubscribe → EventBus)
Phase 4: Wire Up Publishers (migrated from inline calls)
- Shift CRUD + signup (shift.created/updated/deleted, shift.signup.created/cancelled)
- Canvass session complete + visits (canvass.session.completed, contact.address.updated)
- Response submit (response.submitted)
- Campaign email sent (campaign.email.sent)
- Payment/donation/subscription events (3 event types)
- Contact tag changes (contact.tags.changed — 3 call sites)
- Reengagement sent (reengagement.sent)
- Campaign CRUD + publish + moderation (campaign.created/updated/deleted/published/status.changed)
- User create/update/delete/approve (user.created/updated/deleted/approved)
- SMS campaign start/complete + message send/receive (4 event types)
- Media video publish/unpublish/view (3 event types)
- Ticketed event publish/cancel (EventBus publishes alongside existing Gancio calls)
- Impact story publish (social.impact-story.published)
- Meeting create/update/delete (not yet migrated — meetings module needs review)
Phase 5: Future
- Add SHIFT, MEETING, TICKETED_EVENT to CalendarItemSource enum (Prisma migration)
- Migrate remaining Gancio calls (ticketed-events, meeting-planner) to EventBus
- Add engagement scoring listener
- Add Homepage dashboard data listener
Event Catalog
Currently Wired (11 event points, 3 consumers)
| Event | Listmonk | Rocket.Chat | Gancio |
|---|---|---|---|
| shift.signup | yes | yes | - |
| shift.signup.cancelled | - | yes | - |
| shift.created | - | - | yes |
| shift.updated | - | - | yes |
| shift.deleted | - | - | yes |
| canvass.session.completed | yes | yes | - |
| canvass.address.updated | yes | - | - |
| campaign.email.sent | yes | - | - |
| response.submitted | - | yes | - |
| subscription.activated | yes | - | - |
| donation.completed | yes | - | - |
| product.purchased | yes | - | - |
| contact.tags.changed | yes | - | - |
| reengagement.sent | yes | - | - |
New Events (49+ handlers need publishers)
| Event | CRM Activity | Calendar | RC | n8n |
|---|---|---|---|---|
| campaign.created | - | - | - | yes |
| campaign.published | - | - | yes | yes |
| campaign.status.changed | - | - | yes | yes |
| user.approved | - | - | yes | yes |
| user.created | - | - | - | yes |
| video.published | - | - | yes | yes |
| video.viewed | yes | - | - | - |
| sms.message.received | yes | - | yes* | yes |
| sms.campaign.completed | - | - | yes | yes |
| ticketed-event.published | - | yes | - | yes |
| meeting.created | - | yes | - | - |
| impact-story.published | - | - | yes | yes |
| shift.created | - | yes | - | yes |
| donation.completed | yes | - | yes | yes |
| subscription.activated | yes | - | - | yes |
*SMS escalations (QUESTION/NEGATIVE sentiment) to relevant RC channel
Design Decisions
- Listeners self-guard: Each listener checks its own feature flag (ENABLE_CHAT, LISTMONK_SYNC_ENABLED, etc.) — the EventBus doesn't filter
- Error isolation: Each listener wraps its handler in try-catch; one listener failing doesn't affect others
- No persistence: Events are ephemeral — if the server restarts mid-event, it's lost (data is already in DB)
- Stats tracking: EventBus tracks per-event emission counts + per-listener execution counts for observability
- Wildcard subscriptions: Listeners can subscribe to
shift.*to catch all shift events