Notification Service
An Effect notification dispatcher that sends dynamic channel payloads through the installed notification channels matching each top-level message key.
bunx --bun shadcn@latest add https://krakstack.net/r/service-notification.jsonOverview
service-notification provides the core Effect notification dispatcher. Messages are dynamic objects where each top-level key is a channel capability, such as email, discord, sns, or push. Installed channels declare the key they handle, contribute their payload type through NotificationChannels, and validate their own payloads.
The core package intentionally does not include provider-specific dependencies. Install separate channel registry entries, such as notification-channel-email-ses, to add transports.
Included Files
services/notification/index.ts- MainNotificationServiceinterface and layer helpersservices/notification/channels/index.ts- Channel contract and channel registry layerservices/notification/schema.ts- Dynamic message envelope and shared send error
Usage
import { Effect, Layer } from "effect";
import { NotificationService } from "@/services/notification";
import { NotificationChannelRegistry } from "@/services/notification/channels";
import { SesNotificationChannel } from "@/services/notification/channels/ses";
const notificationChannelsLayer = Layer.effect(
NotificationChannelRegistry,
Effect.gen(function* () {
const email = yield* SesNotificationChannel;
return NotificationChannelRegistry.make(email);
}),
).pipe(Layer.provide(SesNotificationChannel.layer));
const notificationServiceLayer = NotificationService.layer.pipe(
Layer.provide(notificationChannelsLayer),
);
const program = Effect.gen(function* () {
const notifications = yield* NotificationService;
yield* notifications.send({
email: {
to: "[email protected]",
subject: "Deploy succeeded",
html: "<h1>main -> production</h1><p>All checks passed.</p>",
},
});
});
await Effect.runPromise(
program.pipe(Effect.provide(notificationServiceLayer)),
);Typed Messages
NotificationService.send is typed from the installed channel declarations. Channel packages extend the core NotificationChannels interface with their key and payload type. For example, the SES channel contributes email: SesEmailNotification, so send({ email: ... }) is checked against the SES payload schema at compile time.
Unknown keys are rejected by TypeScript once at least one channel augments NotificationChannels; missing runtime wiring still fails with NotificationSendError.
Channel Behavior
For each key in the message, the dispatcher sends the payload to every installed channel with the same key. This allows multiple providers to share a capability, such as primary and audit email transports. If a message includes a key with no installed channel, the send fails with NotificationSendError so notification wiring problems are not silently ignored.
Install additional channel entries to enable more keys without changing the core service.
Merging Channels
Use NotificationChannelRegistry.make(...) when you need one NotificationService layer with several channel implementations. Each channel package should expose its channel service layer, then the app-level registry can collect the installed channel instances.
import { Effect, Layer } from "effect";
import { NotificationChannelRegistry } from "@/services/notification/channels";
import { NotificationService } from "@/services/notification";
import { SesNotificationChannel } from "@/services/notification/channels/ses";
import { DiscordNotificationChannel } from "@/services/notification/channels/discord";
const notificationChannelsLayer = Layer.effect(
NotificationChannelRegistry,
Effect.gen(function* () {
const email = yield* SesNotificationChannel;
const discord = yield* DiscordNotificationChannel;
return NotificationChannelRegistry.make(email, discord);
}),
).pipe(
Layer.provideMerge(SesNotificationChannel.layer),
Layer.provideMerge(DiscordNotificationChannel.layer),
);
export const notificationServiceLayer = NotificationService.layer.pipe(
Layer.provide(notificationChannelsLayer),
);A message can now include every merged capability in one send.
yield* notifications.send({
email: {
to: "[email protected]",
subject: "Deploy succeeded",
text: "All checks passed.",
},
discord: {
content: "Deploy succeeded.",
},
});