Skip to content

NOTIFICATIONS MODULE

María Fátima García Luque edited this page Jun 25, 2026 · 1 revision

Notifications Module — @fireflyframework/core/notifications

Dispatch system events to multiple external channels via the Adapter pattern: console, Slack, generic webhook, and email relay. Package: @fireflyframework/core · EXTENDED Module #2 · Platform + Embedded


Table of Contents


1. Overview

The Notifications module provides an event dispatch system based on the Adapter pattern. Each system event (FireflyEvent) is emitted through NotificationService and routed to one or more external channels (console, Slack, webhook, email) based on the registered adapters.

What it does

  • NotificationService — central orchestrator that maintains a registry of adapters and dispatches events to all of them that support the event type. Uses Promise.allSettled for fault tolerance
  • ConsoleAdapter — always-active adapter that formats events in the browser console with a severity prefix ([INFO], [WARN], [ERROR], [CRITICAL])
  • NoopAdapter — empty adapter for test environments. Accepts everything, does nothing
  • SlackAdapter — sends events to a Slack webhook with severity-based emoji formatting
  • WebhookAdapter — sends the full FireflyEvent as JSON to a generic HTTP endpoint. Supports custom headers
  • EmailAdapter — sends events as email through an HTTP relay endpoint (the browser cannot do SMTP directly)

What it does NOT do

  • It does not implement a notification center UI — that is the responsibility of the product layer
  • It does not manage push notifications or WebSockets — only one-way dispatch
  • It does not implement retry queues — if an adapter fails, the event is lost for that channel
  • It does not implement event persistence — there is no server-side history
  • It does not filter by user or role — events are at the system level, not the user level
  • It does not implement rate limiting or deduplication

Why it is a framework piece

Every product needs to notify system events to external channels: critical errors to Slack, deployments to a CI webhook, alerts via email. Without a unified module, each product reimplements the integration with every channel, duplicating severity parsers, message formatting, and HTTP error handling.

The Notifications module provides:

  • A single contract (NotificationAdapter) that abstracts any external channel
  • Selective routing via supports(eventType) so each adapter filters events
  • Fault tolerance via Promise.allSettled — one failing adapter does not block the others
  • Declarative configuration via provideNotifications() with opt-in per channel
  • Extensibility: any product can create custom adapters (e.g., Sentry, PagerDuty, Teams)

Key architectural decision

EXTENDED module (not CORE): Not every application needs to dispatch events to external channels. Embedded applications (micro-frontends) typically delegate notifications to the host. For this reason, the module requires explicit registration via provideNotifications() and the services do not use providedIn: 'root'.

ConsoleAdapter always active: Even without configuring any channel, ConsoleAdapter is registered as a fallback. This guarantees that events always leave an observable trace in development.

Relationship with Flutter

In the original Flutter portal (soon-distributor-portal), there was no internal notification system. Errors were handled with direct prints, Slack/email integrations were done ad hoc from the backend, and there was no unified pattern for dispatching frontend events.

Firefly introduces this module as part of the product's observability layer, allowing the frontend to emit relevant events in a structured way and have them automatically routed to the configured channels.


2. Developer Usage Guide

2.1 Installation and initial configuration

The Notifications module is EXTENDED — it requires explicit registration:

// app.config.ts
import { provideNotifications } from '@fireflyframework/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideNotifications({
      adapters: ['slack', 'email'],
      slack: { webhookUrl: 'https://hooks.slack.com/services/...' },
      email: { smtpEndpoint: 'https://api.internal/send-email', from: 'noreply@acme.com' },
    }),
  ],
};

Without arguments (provideNotifications()) it registers only ConsoleAdapter.

2.2 Emit an event

import { NotificationService } from '@fireflyframework/core';

@Component({ /* ... */ })
export class DeployComponent {
  private readonly notifications = inject(NotificationService);

  async onDeployComplete(): Promise<void> {
    await this.notifications.emit({
      type: 'deploy.complete',
      product: 'my-app',
      severity: 'info',
      message: 'Deployment v2.1.0 completed successfully',
      timestamp: new Date().toISOString(),
      data: { version: '2.1.0', environment: 'production' },
    });
  }
}

The event is sent to every adapter that supports the 'deploy.complete' type. The built-in adapters accept every type by default.

2.3 Register a custom adapter

import { NotificationService, NotificationAdapter, FireflyEvent } from '@fireflyframework/core';

const sentryAdapter: NotificationAdapter = {
  name: 'sentry',
  supports: (eventType) => eventType.startsWith('error.'),
  async send(event: FireflyEvent): Promise<void> {
    Sentry.captureMessage(event.message, {
      level: event.severity === 'critical' ? 'fatal' : event.severity,
      tags: { product: event.product, type: event.type },
      extra: event.data,
    });
  },
};

// In your component or service
const notifications = inject(NotificationService);
notifications.registerAdapter(sentryAdapter);

Duplicate adapters (same name) are silently ignored.

2.4 Selective event filtering

The supports(eventType) method lets each adapter decide which events to process:

// Adapter that only processes errors
const errorOnlyAdapter: NotificationAdapter = {
  name: 'error-tracker',
  supports: (type) => type.startsWith('error.'),
  async send(event) { /* ... */ },
};

// Adapter that processes everything except info
const criticalAdapter: NotificationAdapter = {
  name: 'pagerduty',
  supports: (type) => !type.startsWith('info.'),
  async send(event) { /* ... */ },
};

The built-in adapters (ConsoleAdapter, SlackAdapter, etc.) return true for every type — they process all events.

2.5 Built-in adapters

Adapter Channel Always active Requires config
ConsoleAdapter console.log/warn/error Yes No
NoopAdapter None (test) No No
SlackAdapter Slack webhook No webhookUrl
WebhookAdapter Generic HTTP POST No url
EmailAdapter HTTP relay → email No smtpEndpoint, from

3. API Reference

3.1 Types

EventSeverity

type EventSeverity = 'info' | 'warning' | 'error' | 'critical';

Severity levels for system events. Determines the output format in the adapters (emoji, console method, etc.).

FireflyEvent

interface FireflyEvent {
  readonly type: string;           // Dot-separated identifier (e.g. 'error.unhandled')
  readonly product: string;        // Product or service that originated the event
  readonly severity: EventSeverity;
  readonly message: string;        // Human-readable message
  readonly timestamp: string;      // ISO-8601
  readonly data?: Record<string, unknown>; // Arbitrary payload
}

NotificationAdapter

interface NotificationAdapter {
  readonly name: string;
  supports(eventType: string): boolean;
  send(event: FireflyEvent): Promise<void>;
}

Contract that every adapter must implement. supports() enables selective routing.

NotificationOptions

interface NotificationOptions {
  readonly adapters?: ReadonlyArray<'slack' | 'email' | 'webhook'>;
  readonly slack?: SlackAdapterOptions;
  readonly email?: EmailAdapterOptions;
  readonly webhook?: WebhookAdapterOptions;
}

Input for provideNotifications(). All fields are optional.

NotificationConfig

interface NotificationConfig {
  readonly enabledAdapters: ReadonlyArray<string>;
  readonly slack?: SlackAdapterOptions;
  readonly email?: EmailAdapterOptions;
  readonly webhook?: WebhookAdapterOptions;
}

Resolved configuration stored in the DI container. Injected via NOTIFICATION_CONFIG.

SlackAdapterOptions

interface SlackAdapterOptions {
  readonly webhookUrl: string;
}

EmailAdapterOptions

interface EmailAdapterOptions {
  readonly smtpEndpoint: string;
  readonly from: string;
}

WebhookAdapterOptions

interface WebhookAdapterOptions {
  readonly url: string;
  readonly headers?: Record<string, string>;
}

3.2 NotificationService

Import: import { NotificationService } from '@fireflyframework/core'; Registration: Via provideNotifications() (@Injectable(), not providedIn: 'root')

Method Signature Description
registerAdapter (adapter: NotificationAdapter) => void Registers an adapter. Duplicates (same name) are ignored
emit (event: FireflyEvent) => Promise<void> Emits an event to every adapter that supports it. Uses Promise.allSettled
getAdapters () => ReadonlyArray<NotificationAdapter> Returns a read-only snapshot of the registered adapters

3.3 ConsoleAdapter

Import: import { ConsoleAdapter } from '@fireflyframework/core';

Adapter that logs events to the browser console. Always registered as a fallback.

  • name: 'console'
  • supports(): always true
  • send(): formats as [SEVERITY] [product] type: message. Uses console.error for error/critical, console.warn for warning, console.log for info

3.4 NoopAdapter

Import: import { NoopAdapter } from '@fireflyframework/core';

Adapter for test environments. Accepts every event and resolves immediately without side effects.

  • name: 'noop'
  • supports(): always true
  • send(): no-op (resolves immediately)

It is not registered automatically — it requires a manual registerAdapter() call.

3.5 SlackAdapter

Import: import { SlackAdapter } from '@fireflyframework/core';

Sends events to a Slack webhook. Formats with a severity emoji.

  • name: 'slack'
  • supports(): always true
  • send(): POST to webhookUrl with body { text: "emoji [product] type: message" }
  • Constructor: new SlackAdapter(options: SlackAdapterOptions)

Emojis by severity: info → ℹ️, warning → ⚠️, error → ❌, critical → 🚨

3.6 WebhookAdapter

Import: import { WebhookAdapter } from '@fireflyframework/core';

Sends the full FireflyEvent as JSON to a generic HTTP endpoint.

  • name: 'webhook'
  • supports(): always true
  • send(): POST to url with body = JSON.stringify(event). Includes optional headers
  • Constructor: new WebhookAdapter(options: WebhookAdapterOptions)

3.7 EmailAdapter

Import: import { EmailAdapter } from '@fireflyframework/core';

Sends events as email through an HTTP relay endpoint.

  • name: 'email'
  • supports(): always true
  • send(): POST to smtpEndpoint with body { from, subject, body, data, timestamp }
  • Constructor: new EmailAdapter(options: EmailAdapterOptions)

Subject format: [SEVERITY] [product] type

3.8 provideNotifications()

function provideNotifications(options?: NotificationOptions): EnvironmentProviders

Registers NotificationService, NOTIFICATION_CONFIG, and the selected adapters.

Behavior:

  1. Always registers ConsoleAdapter (fallback)
  2. If options.adapters includes 'slack' and options.slack exists → registers SlackAdapter
  3. If options.adapters includes 'webhook' and options.webhook exists → registers WebhookAdapter
  4. If options.adapters includes 'email' and options.email exists → registers EmailAdapter

Registration runs in APP_INITIALIZER so that adapters are available before components start up.

3.9 NOTIFICATION_CONFIG

const NOTIFICATION_CONFIG: InjectionToken<NotificationConfig>

Injection token for the module's resolved configuration. Provided by provideNotifications().


4. Internal Technical Architecture

4.1 Design principles

  1. Adapter pattern — every external channel implements NotificationAdapter. The service does not know the details of each channel
  2. Fault tolerancePromise.allSettled in emit() guarantees that one failing adapter does not block the others
  3. Selective routingsupports(eventType) lets adapters filter events without coupling that logic to the service
  4. Opt-in per channel — only the adapters declared in options.adapters are instantiated
  5. Guaranteed fallbackConsoleAdapter is always active for minimal observability

4.2 File structure

packages/core/src/lib/notifications/
├── index.ts                          # Public API (16 exports)
├── notification.types.ts             # Types, interfaces, InjectionToken
├── notification.service.ts           # NotificationService (orchestrator)
├── notification.service.spec.ts      # Service tests
├── provide-notifications.ts          # provideNotifications() factory
├── provide-notifications.spec.ts     # Provider tests
└── adapters/
    ├── console-adapter.ts            # ConsoleAdapter
    ├── console-adapter.spec.ts
    ├── noop-adapter.ts               # NoopAdapter
    ├── noop-adapter.spec.ts
    ├── slack-adapter.ts              # SlackAdapter
    ├── slack-adapter.spec.ts
    ├── webhook-adapter.ts            # WebhookAdapter
    ├── webhook-adapter.spec.ts
    ├── email-adapter.ts              # EmailAdapter
    └── email-adapter.spec.ts

4.3 Data flow

provideNotifications(options)
    │
    ├─ creates NotificationConfig
    ├─ registers NotificationService
    └─ APP_INITIALIZER:
         ├─ registerAdapter(ConsoleAdapter)    ← always
         ├─ registerAdapter(SlackAdapter)      ← if 'slack' in adapters
         ├─ registerAdapter(WebhookAdapter)    ← if 'webhook' in adapters
         └─ registerAdapter(EmailAdapter)      ← if 'email' in adapters

emit(event: FireflyEvent)
    │
    ├─ filters adapters → adapter.supports(event.type)
    ├─ targets = adapters that accept
    └─ Promise.allSettled(targets.map(a => a.send(event)))
         ├─ ConsoleAdapter.send() → console.log/warn/error
         ├─ SlackAdapter.send()   → fetch(webhookUrl, POST)
         ├─ WebhookAdapter.send() → fetch(url, POST)
         └─ EmailAdapter.send()   → fetch(smtpEndpoint, POST)

4.4 Internal dependencies

The Notifications module has zero dependencies on other framework modules:

  • Does not depend on Angular's HttpClient (uses native fetch())
  • Does not depend on the Transport/Network module
  • Does not depend on the Storage or Persistence modules
  • Does not depend on the Environment module

This independence is deliberate: the module must work in any Angular context without requiring prior configuration of other modules.

4.5 Architectural decisions

Decision Discarded alternative Reason
Native fetch() in HTTP adapters Angular's HttpClient Reduces coupling. Adapters are plain classes (no @Injectable); they do not need DI
Promise.allSettled in emit() Promise.all with try/catch allSettled never rejects, guaranteeing that every adapter runs
Adapters as plain classes Adapters as Angular services Adapters are instantiated in APP_INITIALIZER with new, not via DI. Keeps things simple
supports() on each adapter Centralized filters in the service It is the adapter's responsibility to decide which events it accepts. Follows the Open/Closed Principle
Duplicates silently ignored Error or warning Prevents accidental double-registration on reinitialization without noise

4.6 Relationship with other framework modules

Module Relationship
Alerts Complementary: Notifications dispatches to external channels (Slack, email). Alerts manages internal UI (dialogs, toasts). A product can create a bridge adapter that connects both (AlertAdapter)
Error Handling The product's error handler can use NotificationService.emit() to report errors to external channels
Environment The product can read configuration URLs from environment and pass them to provideNotifications()
Storage No direct relationship. A future EventBus module (Step 2.14) will cover persistence of internal events

5. Advanced Usage Patterns

Bridge adapter (Notifications → Alerts)

Connects the notifications pipeline to the framework's UI alerts system:

import { NotificationAdapter, FireflyEvent } from '@fireflyframework/core';
import { AlertService } from '@fireflyframework/core';

function createAlertAdapter(alerts: AlertService): NotificationAdapter {
  return {
    name: 'alert',
    supports: () => true,
    async send(event: FireflyEvent): Promise<void> {
      const type = event.severity === 'critical' || event.severity === 'error' ? 'error'
        : event.severity === 'warning' ? 'warning' : 'info';
      await alerts.dialog({
        title: `[${event.severity.toUpperCase()}] ${event.type}`,
        message: event.message,
        type: type as 'info' | 'warning' | 'error' | 'success',
        confirmLabel: 'OK',
      });
    },
  };
}

Scoped adapters per feature

Register adapters in a feature module for feature-specific notifications:

@Component({ /* ... */ })
export class LendingDashboardComponent implements OnInit {
  private readonly notifications = inject(NotificationService);

  ngOnInit(): void {
    this.notifications.registerAdapter({
      name: 'lending-audit',
      supports: (type) => type.startsWith('lending.'),
      async send(event) {
        // Send only lending events to a specific audit trail
        await fetch('/api/audit/lending', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(event),
        });
      },
    });
  }
}

Error reporting pipeline

// error-handler.service.ts
@Injectable({ providedIn: 'root' })
export class AppErrorHandler {
  private readonly notifications = inject(NotificationService);
  private readonly env = inject(EnvironmentService);

  async reportError(error: Error, context?: Record<string, unknown>): Promise<void> {
    await this.notifications.emit({
      type: 'error.unhandled',
      product: this.env.get('APP_NAME'),
      severity: 'error',
      message: error.message,
      timestamp: new Date().toISOString(),
      data: { stack: error.stack, ...context },
    });
  }
}

Testing with NoopAdapter

describe('FeatureComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideNotifications()],
    });

    const service = TestBed.inject(NotificationService);
    service.registerAdapter(new NoopAdapter());
  });

  it('should emit event without side effects', async () => {
    const service = TestBed.inject(NotificationService);
    // NoopAdapter accepts everything and does nothing — safe for tests
    await service.emit({ /* ... */ });
  });
});
  • Alerts Module

Clone this wiki locally