
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
@fluojs/discord
Advanced tools
Webhook-first, transport-agnostic Discord delivery core for Fluo with notifications integration.
English 한국어
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.
DiscordServiceverifyOnModuleInit@fluojs/notificationsnpm install @fluojs/discord @fluojs/notifications
This package requires Node.js >=24.0.0 <27 as its package-owned support contract. Upgrade Node 20 and Node 22 hosts to Node.js >=24.0.0 <27; Node versions below 24 and Node 27+ are unsupported, so move the process to a supported line. Its delivery contract remains transport-agnostic at runtime through explicit fetch-compatible boundaries.
@fluojs/notifications.process.env reads inside the package.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 {}
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.`,
});
}
}
DiscordModule.forRoot(...) and DiscordModule.forRootAsync(...) return a global module by default. The module exports DiscordService, DiscordChannel, DISCORD, and DISCORD_CHANNEL; pass global: false only when migrated code needs those providers to remain visible only to modules that explicitly import the returned module. The option is global?: boolean, not NestJS isGlobal.
The package-level registration surface is intentionally singleton-oriented. DISCORD and DISCORD_CHANNEL are compatibility tokens for the one configured Discord service and notifications channel. Applications that need multiple Discord clients should compose app-owned modules/providers around distinct DiscordTransport instances or expose app-owned facades instead of importing private provider helpers.
DiscordServiceUse 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(...) and DiscordModule.forRootAsync(...) export DiscordService, DiscordChannel, DISCORD, and DISCORD_CHANNEL globally by default. Use the fluo global?: boolean option and set global: false only when migrated code must keep Discord providers local to importing modules; NestJS isGlobal is not supported.DiscordService.send(...) resolves defaultThreadId before delivery.DiscordService.sendMany(...) is a direct DiscordMessage[] batch API that sends messages sequentially and supports continueOnError; it is not a multi-recipient @fluojs/notifications dispatch shortcut.initialization failure phase instead of reclassifying it as shutdown-cleanup; a rejected factory creation likewise remains an initialization failure.ready; attempts before bootstrap, during startup, after failed bootstrap, while shutting down, or after shutdown are rejected before delivery.ready drains its renderer and transport delivery before factory-owned transport resources close. Delivery rejection remains the caller's result and does not make shutdown fail.DiscordService.sendNotification(...) checks lifecycle readiness before invoking a configured renderer and passes the caller's AbortSignal to both DiscordTemplateRenderInput.signal and transport delivery.DiscordService.createPlatformStatusSnapshot() exposes the same status contract as createDiscordPlatformStatusSnapshot(...): lifecycle/readiness, health, transport kind and ownership, default thread configuration, bootstrap verification state, distinct bootstrap initialization versus shutdown cleanup failure diagnostics, and notifications channel dependency details, so callers can observe Discord wiring without reaching into internal options.defaultThreadId and notifications.channel values are trimmed and ignored; the notifications channel defaults to discord.process.env directly. All configuration must enter through explicit options or DI.verifyOnModuleInitSet DiscordModuleOptions.verifyOnModuleInit?: boolean to true when the selected transport can verify its own readiness during application bootstrap. DiscordService.onModuleInit() always resolves the configured transport first; if verifyOnModuleInit is enabled and the resolved transport exposes an optional verify() method, the service awaits transport.verify() before marking the Discord provider ready. Transports that do not implement verify are still valid and simply skip the verification step.
DiscordModule.forRoot({
transport: customDiscordTransport,
verifyOnModuleInit: true,
});
Behavioral contract notes:
verifyOnModuleInit is optional and defaults to false.verify() are called, so webhook-only or app-owned transports do not have to add a no-op verifier.transport.verify() rejects, bootstrap fails with the initialization failure, the service lifecycle moves to failed, and readiness/status snapshots report the provider as not ready. Factory-owned transports are closed exactly once; directly supplied app-owned transports are preserved.DiscordService.createPlatformStatusSnapshot() includes verifiedOnModuleInit and bootstrap verification state so health/readiness tooling can tell whether bootstrap verification was requested without reaching into internal options.@fluojs/notificationsInject 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, metadataBehavioral contract notes:
payload.threadId or a single entry in recipients.payload.threadId is omitted, DiscordService.sendNotification(...) uses the first recipients entry or falls back to defaultThreadId.subject / template markers override both. template is rendered only when a renderer is configured.signal, so renderers can stop caller-cancelled work before transport delivery.DiscordService.sendMany(...) or issue separate notification dispatches; a single notification dispatch never expands multi-recipient fan-out implicitly.Register a DiscordTemplateRenderer when notification templates should produce reusable Discord content, embeds, or components. Keep transport setup in the same module registration and dispatch a template key through @fluojs/notifications.
import type { DiscordTemplateRenderer } from '@fluojs/discord';
const renderer: DiscordTemplateRenderer = {
render(input) {
return {
content: `Order ${String(input.payload.orderId)} was received.`,
embeds: [
{
description: input.subject,
title: 'New order',
},
],
};
},
};
DiscordModule.forRoot({
renderer,
transport: createDiscordWebhookTransport({
fetch: runtime.fetch,
webhookUrl: config.discordWebhookUrl,
}),
});
await notifications.dispatch({
channel: 'discord',
locale: 'en',
metadata: { source: 'orders' },
payload: {
content: 'Order #123 is ready for review.',
orderId: '123',
},
subject: 'New order received',
template: 'orders.received',
});
DiscordService.sendNotification(...) calls the renderer only when both template and renderer are present. The renderer receives { template, payload, subject, locale, metadata, signal }. Explicit payload.content, payload.embeds, and payload.components values take precedence over the corresponding rendered values; when payload.content is undefined, content falls back to rendered content and then to subject. This lets callers override one template result without replacing the renderer or transport configuration.
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(...).
The built-in webhook transport retries transient 408, 429, and 5xx responses and transport-level exceptions. Omit retry to preserve the default of three total attempts with a 250ms exponential-backoff base delay. Configure one workload-specific policy at transport construction when its latency or tolerance differs:
const transport = createDiscordWebhookTransport({
fetch: runtime.fetch,
retry: {
attempts: 5,
baseDelayMs: 500,
},
webhookUrl: config.discordWebhookUrl,
});
attempts includes the initial request and must be an integer from 1 through 10. baseDelayMs must be an integer from 0 through 60000; later waits double from that base. The retry policy applies only to this bounded in-process webhook send. When a notification must survive process restarts, require durable scheduling, or need delivery managed independently of request latency, use @fluojs/notifications with queue-backed delivery instead of increasing webhook attempts.
Behavioral contract notes:
408, 429, and 5xx responses, and also retries transport-level exceptions, using the configured bounded exponential backoff before surfacing an error. Permanent upstream responses are not retried.DiscordSendOptions.signal; an already-aborted signal rejects immediately instead of waiting for the next backoff timer.DiscordSendResult.response; caller-visible DiscordTransportError messages still omit raw upstream response bodies by default, including after rate-limit retries fail.webhookUrl values are rejected immediately as DiscordConfigurationError instead of being retried as delivery failures.The Discord package intentionally does not:
process.envThese limitations are part of the package contract so runtime choice, provider capability, and rollout strategy stay explicit at the application boundary.
DiscordDiscordModule.forRoot(options) / DiscordModule.forRootAsync(options)DiscordModuleOptionsDiscordAsyncModuleOptionsDiscordServiceDiscordService.send(message, options)DiscordService.sendMany(messages, options)DiscordService.sendNotification(notification, options)DiscordService.createPlatformStatusSnapshot()DiscordChannelDISCORDDISCORD_CHANNELCompose 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.
DiscordMessageNormalizedDiscordMessageDiscordWebhookRetryOptionsDiscordWebhookTransportOptionsDiscordFetchLikeDiscordFetchResponseDiscordSendResultDiscordSendOptionsDiscordSendManyOptionsDiscordSendBatchResultDiscordSendFailureDiscordNotificationPayloadDiscordNotificationDispatchRequestDiscordAllowedMentionsDiscordAttachmentDiscordComponentDiscordEmbedDiscordPollDiscordTransportDiscordTransportContextDiscordTransportFactoryDiscordTransportReceiptDiscordTemplateRenderInputDiscordTemplateRenderResultDiscordTemplateRenderercreateDiscordWebhookTransport(options)DiscordService.createPlatformStatusSnapshot()createDiscordPlatformStatusSnapshot(...)DiscordLifecycleStateDiscordPlatformStatusSnapshotDiscordStatusAdapterInputDiscordConfigurationErrorDiscordMessageValidationErrorDiscordTransportError@fluojs/notifications: Shared orchestration layer that consumes DISCORD_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.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.FAQs
Webhook-first, transport-agnostic Discord delivery core for Fluo with notifications integration.
The npm package @fluojs/discord receives a total of 7 weekly downloads. As such, @fluojs/discord popularity was classified as not popular.
We found that @fluojs/discord demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.