@fluojs/discord
v1.0.4
Published
Webhook-first, transport-agnostic Discord delivery core for Fluo with notifications integration.
Downloads
321
Maintainers
Readme
@fluojs/discord
Webhook-first, transport-agnostic Discord delivery core for fluo. It provides a Nest-like module API, an injectable DiscordService for standalone usage, and a first-party DiscordChannel for @fluojs/notifications integration without assuming a Node-only Discord SDK.
Migration boundary: the module API is intentionally Nest-like but not a NestJS dynamic-module clone. DiscordModule is global by default through global: options.global ?? true, forRootAsync(...) supports only inject plus useFactory, and internal provider helpers/tokens stay private so applications compose Discord through the module facade and exported service/channel tokens.
Table of Contents
Installation
npm install @fluojs/discord @fluojs/notificationsThis package follows the repo-wide Node.js 20+ install baseline reflected in published package metadata, while keeping its delivery contract transport-agnostic at runtime through explicit fetch-compatible boundaries.
When to Use
- When you want one package that can send Discord messages directly and also plug into
@fluojs/notifications. - When transport choice must stay explicit and portable across Node, Bun, Deno, and Cloudflare-compatible application boundaries.
- When Discord delivery should prefer incoming webhooks while still allowing richer REST or bot-backed integrations through a custom transport contract.
- When configuration must enter through DI or explicit options instead of
process.envreads inside the package.
Quick Start
Register the module
import { Module } from '@fluojs/core';
import { DiscordModule, createDiscordWebhookTransport } from '@fluojs/discord';
@Module({
imports: [
DiscordModule.forRoot({
defaultThreadId: 'release-thread-id',
transport: createDiscordWebhookTransport({
fetch: globalThis.fetch.bind(globalThis),
webhookUrl: 'https://discord.com/api/webhooks/123/abc',
}),
}),
],
})
export class AppModule {}Send Discord messages directly
import { Inject } from '@fluojs/core';
import { DiscordService } from '@fluojs/discord';
@Inject(DiscordService)
export class DeployNotifier {
constructor(private readonly discord: DiscordService) {}
async announce(version: string) {
await this.discord.send({
content: `Deploy ${version} finished successfully.`,
});
}
}Common Patterns
Standalone delivery with DiscordService
Use DiscordService when your application wants direct Discord delivery without routing through the notifications foundation.
DiscordModule.forRootAsync({
inject: [ConfigService],
useFactory: (config) => ({
defaultThreadId: config.discord.defaultThreadId,
transport: createDiscordWebhookTransport({
fetch: config.runtime.fetch,
webhookUrl: config.discord.webhookUrl,
}),
}),
});forRootAsync(...) accepts the fluo async shape only: register dependencies elsewhere in the application graph, list their tokens in inject, and return final DiscordModuleOptions from useFactory. It does not consume NestJS imports, useClass, or useExisting variants, so migrate those patterns to application-owned providers before passing resolved options to Discord.
Behavioral contract notes:
DiscordModule.forRoot(...)andDiscordModule.forRootAsync(...)exportDiscordService,DiscordChannel,DISCORD, andDISCORD_CHANNELglobally by default. Use the fluoglobal?: booleanoption and setglobal: falseonly when migrated code must keep Discord providers local to importing modules; NestJSisGlobalis not supported.DiscordService.send(...)resolvesdefaultThreadIdbefore delivery.DiscordService.sendMany(...)is a directDiscordMessage[]batch API that sends messages sequentially and supportscontinueOnError; it is not a multi-recipient@fluojs/notificationsdispatch shortcut.- The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown, including any in-flight factory-created transport before shutdown began.
- Sends are accepted only after bootstrap marks the transport
ready; attempts before bootstrap, during startup, after failed bootstrap, while shutting down, or after shutdown are rejected before delivery. - Sends attempted while the service is shutting down or already stopped are rejected before reusing the cached transport.
DiscordService.createPlatformStatusSnapshot()exposes the same status contract ascreateDiscordPlatformStatusSnapshot(...): lifecycle/readiness, health, transport kind and ownership, default thread configuration, bootstrap verification state, and notifications channel dependency details, so callers can observe Discord wiring without reaching into internal options.- Blank
defaultThreadIdandnotifications.channelvalues are trimmed and ignored; the notifications channel defaults todiscord. - The package never reads
process.envdirectly. All configuration must enter through explicit options or DI.
Integration with @fluojs/notifications
Inject DISCORD_CHANNEL into NotificationsModule.forRootAsync(...) so the Discord package remains the only place that understands Discord-specific payload fields and recipient-to-thread translation.
import { Module } from '@fluojs/core';
import { NotificationsModule } from '@fluojs/notifications';
import {
DISCORD_CHANNEL,
DiscordModule,
createDiscordWebhookTransport,
} from '@fluojs/discord';
@Module({
imports: [
DiscordModule.forRoot({
transport: createDiscordWebhookTransport({
fetch: globalThis.fetch.bind(globalThis),
webhookUrl: 'https://discord.com/api/webhooks/123/abc',
}),
}),
NotificationsModule.forRootAsync({
inject: [DISCORD_CHANNEL],
useFactory: (channel) => ({
channels: [channel],
}),
}),
],
})
export class AppModule {}Supported notification payload fields:
content,embeds,components,attachmentsallowedMentions,username,avatarUrl,ttsthreadId,threadName,flags,poll,metadata
Behavioral contract notes:
- One notification dispatch maps to exactly one Discord thread route. Use
payload.threadIdor a single entry inrecipients. - If
payload.threadIdis omitted,DiscordService.sendNotification(...)uses the firstrecipientsentry or falls back todefaultThreadId. - Notification metadata is merged from payload metadata, dispatch metadata, and template/subject markers.
templateis rendered only when a renderer is configured. - If a notification workflow needs fan-out across multiple Discord threads, create one concrete Discord message per thread with
DiscordService.sendMany(...)or issue separate notification dispatches; a single notification dispatch never expands multi-recipient fan-out implicitly.
Webhook-first delivery with explicit fetch injection
Use createDiscordWebhookTransport(...) when you want a portable first-party transport that only depends on a fetch-compatible HTTP boundary.
const transport = createDiscordWebhookTransport({
fetch: runtime.fetch,
webhookUrl: discordWebhookUrl,
});
await discord.send({
content: 'Deploy finished',
embeds: [{ description: 'Build 124 succeeded.' }],
});For richer API integrations such as bot-backed REST delivery, implement the exported DiscordTransport contract and inject it through DiscordModule.forRoot(...) or forRootAsync(...).
Behavioral contract notes:
- The built-in webhook transport retries transient
408,429, and5xxresponses, and also retries transport-level exceptions, using bounded exponential backoff before surfacing an error. Permanent upstream responses are not retried. - Successful webhook responses are exposed through
DiscordSendResult.response; caller-visibleDiscordTransportErrormessages still omit raw upstream response bodies by default, including after rate-limit retries fail. - Malformed or non-absolute
webhookUrlvalues are rejected immediately asDiscordConfigurationErrorinstead of being retried as delivery failures.
Intentional limitations
The Discord package intentionally does not:
- read credentials or webhook URLs from
process.env - ship a Node-only Discord SDK inside the shared root package boundary
- force one provider strategy beyond the webhook-first helper and exported transport contract
- expose internal provider helpers, normalized option tokens, or NestJS-style custom provider replacement seams for application imports
- translate one notification into multi-thread fan-out inside a single dispatch call
These limitations are part of the package contract so runtime choice, provider capability, and rollout strategy stay explicit at the application boundary.
Public API Overview
Core
DiscordDiscordModule.forRoot(options)/DiscordModule.forRootAsync(options)DiscordModuleOptionsDiscordAsyncModuleOptionsDiscordServiceDiscordService.send(message, options)DiscordService.sendMany(messages, options)DiscordService.sendNotification(notification, options)DiscordService.createPlatformStatusSnapshot()DiscordChannelDISCORDDISCORD_CHANNEL
Compose applications through DiscordModule and integrate notifications through DISCORD_CHANNEL plus the exported transport contracts.
The package intentionally keeps createDiscordProviders(...), DISCORD_OPTIONS, and NormalizedDiscordModuleOptions out of the public root barrel. If a migration previously customized NestJS internals or provider tokens, wrap DiscordModule.forRoot(...) / forRootAsync(...) in an app-owned module instead of importing private helpers.
Contracts and helpers
DiscordMessageNormalizedDiscordMessageDiscordWebhookTransportOptionsDiscordFetchLikeDiscordFetchResponseDiscordSendResultDiscordSendOptionsDiscordSendManyOptionsDiscordSendBatchResultDiscordSendFailureDiscordNotificationPayloadDiscordNotificationDispatchRequestDiscordAllowedMentionsDiscordAttachmentDiscordComponentDiscordEmbedDiscordPollDiscordTransportDiscordTransportContextDiscordTransportFactoryDiscordTransportReceiptDiscordTemplateRenderInputDiscordTemplateRenderResultDiscordTemplateRenderercreateDiscordWebhookTransport(options)
Status and errors
DiscordService.createPlatformStatusSnapshot()createDiscordPlatformStatusSnapshot(...)DiscordLifecycleStateDiscordPlatformStatusSnapshotDiscordStatusAdapterInputDiscordConfigurationErrorDiscordMessageValidationErrorDiscordTransportError
Related Packages
@fluojs/notifications: Shared orchestration layer that consumesDISCORD_CHANNEL.@fluojs/config: Recommended for resolving webhook URLs or thread ids without direct environment access.@fluojs/event-bus: Useful when Discord notifications are one side effect among several event-driven workflows.
Example Sources
packages/discord/src/module.test.ts: Module registration, async wiring, webhook transport, and notifications integration examples.packages/discord/src/public-surface.test.ts: Public export and TypeScript contract verification.packages/discord/src/status.test.ts: Health/readiness contract examples.
