Event Bus Architecture

Meister Bill uses a type-safe event bus pattern for decoupled component communication across frontend and backend.

Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Component     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ emit(EVENT_PAYMENT_BOOKED)
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Event Bus     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β–Ό          β–Ό          β–Ό          β–Ό
      β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”
      β”‚Toast β”‚  β”‚Discordβ”‚  β”‚WebSocketβ”‚ β”‚Auditβ”‚
      β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜

Generic Event System (v2.0)

Since Issue #188, all events use a consistent structure:

Field Type Description
type create\|update\|delete\|notify Event action
entity user\|customer\|invoice\|product\|... Affected entity
entityId string UUID of affected entity
actor string UUID of user who triggered
details object Event-specific data
metadata object? IP, user agent, session ID

Usage

Frontend: Emitting Events

import { useEventBus } from "~/composables/useEventBus";
import { EVENT_PAYMENT_BOOKED } from "@meisterbill/schemas";

const eventBus = useEventBus();

eventBus.emit(EVENT_PAYMENT_BOOKED, {
  data: {
    invoiceId: invoice.id,
    amount: 100.00,
    currency: "EUR",
    invoiceNumber: "INV-2026-001"
  }
});

Frontend: Creating Subscribers

// apps/web/subscribers/slackSubscriber.ts
export const slackSubscriber = () => {
  const eventBus = useEventBus();
  const config = useRuntimeConfig();

  if (!config.public.slackWebhookUrl) return;

  eventBus.on(EVENT_PAYMENT_BOOKED, async (event) => {
    await fetch(config.public.slackWebhookUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text: `πŸ’° Payment: ${event.data.amount} ${event.data.currency}`
      })
    });
  });
};

Enable in apps/web/plugins/05.EventBusSubscribers.ts.

Backend: Emitting Events

import { emitEvent } from "../services/EventBus";

emitEvent("create", "invoice", invoice.id, userId, {
  document_number: invoice.document_number,
  amount: invoice.total
});

The AuditSubscriber automatically logs all events to the database.

Available Events

Event Data
payment.booked invoiceId, amount, currency, invoiceNumber
payment.failed invoiceId, amount, error
invoice.created invoiceId, invoiceNumber, customerName
invoice.updated invoiceId, invoiceNumber
invoice.deleted invoiceId, invoiceNumber
customer.created customerId, customerName
auth.login.success email
auth.register.failed email, error

Configuration

Subscriber Environment Variable
Discord NUXT_PUBLIC_DISCORD_WEBHOOK_URL
WebSocket NUXT_PUBLIC_WEBSOCKET_URL

Testing

// Mock event bus in tests
const mockEmit = vi.fn();
vi.mock("~/composables/useEventBus", () => ({
  useEventBus: () => ({ emit: mockEmit, on: vi.fn(), off: vi.fn() })
}));

expect(mockEmit).toHaveBeenCalledWith(EVENT_PAYMENT_BOOKED, {
  data: expect.objectContaining({ amount: 100 })
});

Best Practices

  1. Use constants: Import event names from @meisterbill/schemas
  2. Keep data minimal: Only include necessary information
  3. Async handlers: Don't block on event handlers
  4. Naming: Use resource.action pattern
  5. Debug: Use console.debug for visibility

Migration Example

Before:

const { add: addToast } = useToast();
try {
  await bookPayment(data);
  addToast({ title: "Success", color: "success" });
} catch (error) {
  addToast({ title: "Failed", color: "error" });
}

After:

const eventBus = useEventBus();
try {
  await bookPayment(data);
  eventBus.emit(EVENT_PAYMENT_BOOKED, { data: { ... } });
} catch (error) {
  eventBus.emit(EVENT_PAYMENT_FAILED, { data: { error: error.message } });
}

Files

  • schemas/src/EventSchema.ts - Event definitions
  • apps/web/composables/useEventBus.ts - Frontend event bus
  • apps/api/src/services/EventBus.ts - Backend event bus
  • apps/web/plugins/05.EventBusSubscribers.ts - Subscriber initialization

See Testing Guide for testing strategies.