Skip to main content
← Back to list
01Issue
FeatureOpenSwamp Club
AssigneesNone

Introduce domain events to formalize the telemetry-to-consumer pipeline

Opened by stack72 · 4/9/2026· GitHub #255

Context

From the DDD analysis of #253 (comment).

Current architecture

The codebase already has a working event pipeline:

track() → telemetry service → telemetry_events collection
                                    ↓
                              ┌─────┴──────┐
                              │  consumers  │
                              ├─────────────┤
                              │ stats       │ → username_metrics
                              │ discord bot │ → Discord channel
                              └─────┬───────┘
                                    ↓
                          swamp_club_events queue
                                    ↓
                          score refresh + badge claims

This works and is decoupled at the infrastructure level — the telemetry service, Discord bot, and score pipeline are all separate services with independent polling loops. The Discord bot registers handlers per event name and uses cursor-based pagination to avoid duplicate messages.

The gap

The coupling is implicit at the domain modeling level:

  1. Producers are ad-hoc. Route handlers (and BetterAuth hooks) decide what to track by calling track(eventName, ...) with string event names and untyped property bags. There's no contract between what producers emit and what consumers expect.

  2. Consumers match on strings. The Discord bot registers handlers for "extension_published" and "sign_up" by name. If a producer renames an event or changes its properties, consumers break silently.

  3. No domain concept of "something happened." The domain layer (lib/domain/) has no notion of events. Business-significant occurrences (collective created, member removed, extension published) are only captured as a side effect in the adapter layer, not as an explicit part of the domain model.

Proposal

Introduce a lightweight domain events layer:

1. Typed event definitions

// lib/domain/events.ts
interface CollectiveCreated {
  type: "collective_created";
  collectiveId: string;
  slug: string;
  name: string;
  createdByUserId: string;
  createdByUsername: string;
}

interface ExtensionPublished {
  type: "extension_published";
  extensionName: string;
  version: string;
  userId: string;
  username: string;
  collectiveSlug?: string;
}

type DomainEvent = CollectiveCreated | ExtensionPublished | ...;

This gives producers and consumers a shared, typed contract.

2. Emit from app services / domain operations

Application services (or aggregates, where it makes sense) emit domain events as part of use case execution, rather than route handlers calling track() directly. This addresses #254 as well.

3. Dispatch to existing infrastructure

A thin adapter maps DomainEventtrack() call, preserving the existing telemetry service pipeline. The Discord bot, score system, and Mixpanel all continue to work — they just consume from a formalized source.

Benefits

  • Typed contract between producers and consumers — rename an event field and get a compile error, not a silent break
  • Domain-level visibility into what events the system considers significant
  • Easy to add consumers without touching producing code
  • Natural fit for the existing infrastructure — the telemetry service already acts as an event broker

Non-goals

  • Not proposing to replace the telemetry service or Discord bot architecture
  • Not proposing full event sourcing
  • Not proposing async event buses or message queues beyond what already exists

Open questions

  • Should events be emitted from aggregates (pure DDD) or app services (pragmatic)? Aggregates keep events closest to invariants; app services have access to cross-aggregate context.
  • Should the shared contract live in lib/domain/events.ts or shared/ (since the telemetry service and Discord bot also need the types)?
  • Migration path: can we introduce typed events incrementally alongside existing track() calls?
  • #253 — Collective lifecycle telemetry (adds more events to the current ad-hoc pattern)
  • #254 — Move telemetry from routes to app services (prerequisite or co-requisite)
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

4/9/2026, 4:44:21 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.