New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@fluojs/discord

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/discord

Webhook-first, transport-agnostic Discord delivery core for Fluo with notifications integration.

Source
npmnpm
Version
2.0.0
Version published
Weekly downloads
12
-36.84%
Maintainers
1
Weekly downloads
 
Created
Source

@fluojs/discord

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.

Table of Contents

Installation

npm 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.

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.env reads 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

Module visibility and migration boundaries

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.

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(...) 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.
  • The service initializes the configured transport during module bootstrap and closes factory-owned resources exactly once across bootstrap verification failure and application shutdown, including any in-flight factory-created transport before shutdown began. Shutdown waits for in-flight bootstrap verification to settle before closing a factory-owned transport. If verification fails and the subsequent owned cleanup also fails, status diagnostics preserve the first initialization failure phase instead of reclassifying it as shutdown-cleanup; a rejected factory creation likewise remains an initialization failure.
  • 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.
  • A direct send or notification dispatch accepted while 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.
  • Sends attempted while the service is shutting down or already stopped are rejected before reusing the cached transport.
  • 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.
  • Blank defaultThreadId and notifications.channel values are trimmed and ignored; the notifications channel defaults to discord.
  • The package never reads process.env directly. All configuration must enter through explicit options or DI.

Bootstrap verification with verifyOnModuleInit

Set 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.
  • Verification is capability-based: only transports that expose verify() are called, so webhook-only or app-owned transports do not have to add a no-op verifier.
  • If 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.

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, attachments
  • allowedMentions, username, avatarUrl, tts
  • threadId, threadName, flags, poll, metadata

Behavioral contract notes:

  • One notification dispatch maps to exactly one Discord thread route. Use payload.threadId or a single entry in recipients.
  • If payload.threadId is omitted, DiscordService.sendNotification(...) uses the first recipients entry or falls back to defaultThreadId.
  • Notification metadata is merged from payload metadata, dispatch metadata, and template/subject markers. On duplicate keys, dispatch metadata overrides payload metadata, and final subject / template markers override both. template is rendered only when a renderer is configured.
  • Template rendering starts only while the service is ready. The render input includes signal, so renderers can stop caller-cancelled work before transport delivery.
  • 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.

Template rendering with payload overrides

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.

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(...).

Configurable webhook retry policy

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:

  • The built-in webhook transport retries transient 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.
  • Retry backoff observes DiscordSendOptions.signal; an already-aborted signal rejects immediately instead of waiting for the next backoff timer.
  • Successful webhook responses are exposed through DiscordSendResult.response; caller-visible DiscordTransportError messages still omit raw upstream response bodies by default, including after rate-limit retries fail.
  • Malformed or non-absolute webhookUrl values are rejected immediately as DiscordConfigurationError instead 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

  • Discord
  • DiscordModule.forRoot(options) / DiscordModule.forRootAsync(options)
  • DiscordModuleOptions
  • DiscordAsyncModuleOptions
  • DiscordService
  • DiscordService.send(message, options)
  • DiscordService.sendMany(messages, options)
  • DiscordService.sendNotification(notification, options)
  • DiscordService.createPlatformStatusSnapshot()
  • DiscordChannel
  • DISCORD
  • DISCORD_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

  • DiscordMessage
  • NormalizedDiscordMessage
  • DiscordWebhookRetryOptions
  • DiscordWebhookTransportOptions
  • DiscordFetchLike
  • DiscordFetchResponse
  • DiscordSendResult
  • DiscordSendOptions
  • DiscordSendManyOptions
  • DiscordSendBatchResult
  • DiscordSendFailure
  • DiscordNotificationPayload
  • DiscordNotificationDispatchRequest
  • DiscordAllowedMentions
  • DiscordAttachment
  • DiscordComponent
  • DiscordEmbed
  • DiscordPoll
  • DiscordTransport
  • DiscordTransportContext
  • DiscordTransportFactory
  • DiscordTransportReceipt
  • DiscordTemplateRenderInput
  • DiscordTemplateRenderResult
  • DiscordTemplateRenderer
  • createDiscordWebhookTransport(options)

Status and errors

  • DiscordService.createPlatformStatusSnapshot()
  • createDiscordPlatformStatusSnapshot(...)
  • DiscordLifecycleState
  • DiscordPlatformStatusSnapshot
  • DiscordStatusAdapterInput
  • DiscordConfigurationError
  • DiscordMessageValidationError
  • DiscordTransportError
  • @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.

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.

Keywords

fluo

FAQs

Package last updated on 07 Sep 2026

Related posts