npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fluojs/slack

v2.0.2

Published

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

Readme

@fluojs/slack

Webhook-first, transport-agnostic Slack delivery core for fluo. It provides a Nest-like module API, an injectable SlackService for standalone usage, and a first-party SlackChannel for @fluojs/notifications integration without assuming a Node-only SDK.

Table of Contents

Installation

npm install @fluojs/slack @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 Slack 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 Slack delivery should prefer incoming webhooks while still allowing richer API 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 { SlackModule, createSlackWebhookTransport } from '@fluojs/slack';

@Module({
  imports: [
    SlackModule.forRoot({
      defaultChannel: '#ops',
      transport: createSlackWebhookTransport({
        fetch: globalThis.fetch.bind(globalThis),
        webhookUrl: 'https://hooks.slack.com/services/T000/B000/XXXX',
      }),
    }),
  ],
})
export class AppModule {}

Send Slack messages directly

import { Inject } from '@fluojs/core';
import { SlackService } from '@fluojs/slack';

@Inject(SlackService)
export class DeployNotifier {
  constructor(private readonly slack: SlackService) {}

  async announce(version: string) {
    await this.slack.send({
      text: `Deploy ${version} finished successfully.`,
    });
  }
}

Common Patterns

Module visibility and migration boundaries

SlackModule.forRoot(...) and SlackModule.forRootAsync(...) return a global module by default. The module exports SlackService, SlackChannel, and SLACK_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.

Async registration supports the injected factory shape only: SlackModule.forRootAsync({ inject, useFactory, global? }). It consumes inject and useFactory, not NestJS imports, useClass, or useExisting. Register dependencies in the application module graph first, list their tokens in inject, then return the final Slack options from useFactory.

The package-level registration surface is intentionally singleton-oriented. SLACK_CHANNEL identifies the one configured notifications channel; application code injects SlackService for direct provider delivery. Applications that need multiple Slack clients should compose their own modules/providers around distinct SlackTransport instances instead of expecting a package-level multi-client registry.

Standalone delivery with SlackService

Use SlackService when your application wants direct Slack delivery without routing through the notifications foundation.

SlackModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config) => ({
    defaultChannel: config.slack.defaultChannel,
    transport: createSlackWebhookTransport({
      fetch: config.runtime.fetch,
      webhookUrl: config.slack.webhookUrl,
    }),
  }),
});

Behavioral contract notes:

  • SlackService.send(...) resolves defaultChannel before delivery.
  • SlackService.sendMany(...) sends messages sequentially and supports continueOnError when callers need a batch result instead of fail-fast behavior; it collects ordinary provider failures only, while caller cancellation always rejects the batch.
  • SlackService.send(...), SlackService.sendMany(...), and SlackService.sendNotification(...) honor already-aborted signals before provider handoff. An aborted signal or a transport AbortError takes precedence over continueOnError, and the same signal is propagated through notification channels to transport calls.
  • The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
  • Direct and notification-backed delivery require the lifecycle to be ready; calls before onModuleInit() finishes, after initialization failure, or during shutdown fail with SlackLifecycleError instead of lazily creating or reusing transports.
  • Shutdown rejects new deliveries, waits for the shared bootstrap initialization promise—including any in-flight factory-owned transport creation and verify() call—to settle, drains active deliveries, and then closes the owned transport before completion.
  • Factory-owned transport cleanup is serialized across bootstrap-failure cleanup and application shutdown, so the same owned transport is closed at most once even when those paths race.
  • SlackService.createPlatformStatusSnapshot() reports lifecycle, readiness, and transport ownership without requiring callers to reach into internal options.
  • The package never reads process.env directly. All configuration must enter through explicit options or DI.

Bootstrap verification with verifyOnModuleInit

Set SlackModuleOptions.verifyOnModuleInit?: boolean to true when the selected transport can verify its own readiness during application bootstrap. SlackService.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 Slack provider ready. Transports that do not implement verify are still valid and simply skip the verification step.

import { Module } from '@fluojs/core';
import {
  SlackModule,
  type SlackTemplateRenderer,
  type SlackTransport,
} from '@fluojs/slack';

const renderer: SlackTemplateRenderer = {
  render({ template, payload, subject }) {
    if (template === 'deploy.finished') {
      return {
        blocks: [
          {
            text: { text: `*${String(payload.version)}* is live`, type: 'mrkdwn' },
            type: 'section',
          },
        ],
        text: subject,
      };
    }

    return { text: subject };
  },
};

const slackApiTransport: SlackTransport = {
  async verify() {
    await slackApi.authTest();
  },
  async send(message, { signal }) {
    const receipt = await slackApi.postMessage(message, { signal });

    return {
      channel: receipt.channel,
      messageTs: receipt.ts,
      ok: receipt.ok,
    };
  },
};

@Module({
  imports: [
    SlackModule.forRoot({
      defaultChannel: '#ops',
      renderer,
      transport: slackApiTransport,
      verifyOnModuleInit: true,
    }),
  ],
})
export class AppModule {}

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.
  • Bootstrap initialization and verification share one promise with shutdown, so a factory-owned transport remains open until an in-flight verify() call settles and is then closed at most once.
  • If transport.verify() rejects, bootstrap fails with SlackLifecycleError wrapping the initialization failure, the service lifecycle moves to failed, readiness/status snapshots report the provider as not ready, and factory-owned transports that were already resolved are closed before the error is rethrown. If shutdown starts while that cleanup is still in progress, both callers await the same close work.
  • SlackService.createPlatformStatusSnapshot() includes verifiedOnModuleInit so health/readiness tooling can tell whether bootstrap verification was requested.

Integration with @fluojs/notifications

Inject SLACK_CHANNEL into NotificationsModule.forRootAsync(...) so the Slack package remains the only place that understands Slack-specific payload fields and recipient-to-channel translation.

import { Module } from '@fluojs/core';
import { NotificationsModule } from '@fluojs/notifications';
import {
  SLACK_CHANNEL,
  SlackModule,
  createSlackWebhookTransport,
} from '@fluojs/slack';

@Module({
  imports: [
    SlackModule.forRoot({
      transport: createSlackWebhookTransport({
        fetch: globalThis.fetch.bind(globalThis),
        webhookUrl: 'https://hooks.slack.com/services/T000/B000/XXXX',
      }),
    }),
    NotificationsModule.forRootAsync({
      inject: [SLACK_CHANNEL],
      useFactory: (channel) => ({
        channels: [channel],
      }),
    }),
  ],
})
export class AppModule {}

Supported notification payload fields:

  • text, blocks, attachments
  • threadTs, replyBroadcast
  • username, iconEmoji, iconUrl
  • mrkdwn, unfurlLinks, unfurlMedia, metadata

Behavioral contract notes:

  • One notification dispatch maps to exactly one Slack destination from a single entry in recipients.
  • If recipients is omitted, SlackService.sendNotification(...) falls back to defaultChannel.
  • Notification metadata is merged from payload metadata, dispatch metadata, and subject/template markers before delivery.
  • If a notification needs fan-out across multiple Slack destinations, call sendMany(...) instead of one multi-recipient dispatch.

Template rendering and merge precedence

Provide a SlackTemplateRenderer when notification templates should turn shared @fluojs/notifications envelopes into Slack-specific text, blocks, or attachments. SlackService.sendNotification(...) calls renderer.render(input) only when both notification.template and SlackModuleOptions.renderer are present. The render input includes template, payload, optional subject, optional locale, and optional dispatch metadata.

import type { SlackTemplateRenderer } from '@fluojs/slack';

const renderer: SlackTemplateRenderer = {
  async render(input) {
    return {
      blocks: [
        {
          text: {
            text: `*${String(input.payload.releaseId)}* deployed for ${input.locale ?? 'default'}`,
            type: 'mrkdwn',
          },
          type: 'section',
        },
      ],
      text: input.subject,
    };
  },
};

await notifications.dispatch({
  channel: 'slack',
  locale: 'en',
  metadata: { source: 'ci' },
  payload: {
    metadata: { releaseId: 'rel-42' },
    text: 'Deploy rel-42 finished.',
  },
  recipients: ['#ops'],
  subject: 'Deploy finished',
  template: 'deploy.finished',
});

Merge precedence is deterministic:

  • payload.attachments and payload.blocks win over rendered attachments and blocks when those payload fields are defined. Non-blank payload.text wins over rendered text.
  • Text falls back from non-blank payload.text to non-blank rendered text, then to non-blank notification.subject. Empty or whitespace-only text values are treated as unspecified.
  • Metadata is merged as payload metadata, dispatch metadata, subject marker, then template marker. Later entries win on duplicate keys, so the final message records the dispatch subject and template markers when present.
  • If no template is set or no renderer is registered, no template rendering occurs; the notification is adapted from its payload and subject only.

Webhook-first delivery with explicit fetch injection

Use createSlackWebhookTransport(...) when you want a portable first-party transport that only depends on a fetch-compatible HTTP boundary.

const transport = createSlackWebhookTransport({
  fetch: runtime.fetch,
  webhookUrl: slackWebhookUrl,
});

await slack.send({
  blocks: [{ type: 'section', text: { type: 'mrkdwn', text: '*Deploy finished*' } }],
  text: 'Deploy finished',
});

For richer API integrations such as chat.postMessage, implement the exported SlackTransport contract and inject it through SlackModule.forRoot(...) or forRootAsync(...).

Behavioral contract notes:

  • Passing fetch explicitly is the portable path and is recommended for all supported runtimes. For backward compatibility, omitting fetch falls back to globalThis.fetch when that ambient runtime API exists; runtimes without globalThis.fetch fail fast with SlackConfigurationError.
  • The built-in webhook transport consumes transient 408, 429, and 5xx response bodies before retrying with bounded exponential backoff, then surfaces an error only after retries are exhausted.
  • Abort signals are passed to the injected fetch boundary and cancel retry backoff without wrapping AbortError values as SlackTransportError.
  • Caller-visible SlackTransportError messages omit raw upstream response bodies by default.

Intentional limitations

The Slack package intentionally does not:

  • read credentials or webhook URLs from process.env
  • ship a Node-only Slack SDK inside the shared root package boundary
  • force one provider strategy beyond the webhook-first helper and exported transport contract
  • provide a package-level multi-client registry beyond the singleton module surface
  • translate one notification into multi-channel 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

  • SlackModule.forRoot(options) / SlackModule.forRootAsync(options)
  • SlackModuleOptions
  • SlackAsyncModuleOptions
  • SlackService
  • SlackService.send(message, options)
  • SlackService.sendMany(messages, options)
  • SlackService.sendNotification(notification, options)
  • SlackService.createPlatformStatusSnapshot()
  • SlackChannel
  • SLACK_CHANNEL

Service and result contracts

  • SlackSendOptions
  • SlackSendManyOptions
  • SlackSendResult
  • SlackSendBatchResult
  • SlackSendFailure

Contracts and helpers

  • SlackMessage
  • NormalizedSlackMessage
  • SlackNotificationPayload
  • SlackNotificationDispatchRequest
  • SlackBlock
  • SlackAttachment
  • SlackTransport
  • SlackTransportFactory
  • SlackTransportContext
  • SlackTransportReceipt
  • SlackFetchLike
  • SlackFetchResponse
  • SlackWebhookTransportOptions
  • createSlackWebhookTransport(options)
  • SlackTemplateRenderer
  • SlackTemplateRenderInput
  • SlackTemplateRenderResult

Status and errors

  • createSlackPlatformStatusSnapshot(...)
  • SlackPlatformStatusSnapshot
  • SlackLifecycleState
  • SlackStatusAdapterInput
  • SlackConfigurationError
  • SlackLifecycleError: thrown by lifecycle-gated delivery before readiness, after initialization failure, or during shutdown, plus transport factory, verification, and owned-resource cleanup failures. Catch this error when sends can race with bootstrap or application teardown.
  • SlackMessageValidationError: thrown when a direct message has no Slack-visible text, blocks, or attachments, or when one notification dispatch resolves multiple Slack recipients and should be split with sendMany(...).
  • SlackTransportError

Related Packages

  • @fluojs/notifications: Shared orchestration layer that consumes SLACK_CHANNEL.
  • @fluojs/config: Recommended for resolving webhook URLs or tokens without direct environment access.
  • @fluojs/event-bus: Useful when Slack notifications are one side effect among several event-driven workflows.

Example Sources

  • packages/slack/src/module.test.ts: Module registration, async wiring, webhook transport, and notifications integration examples.
  • packages/slack/src/lifecycle-regression.test.ts: Bootstrap verification and shutdown ordering regression coverage.
  • packages/slack/src/public-surface.test.ts: Public export and TypeScript contract verification.
  • packages/slack/src/status.test.ts: Health/readiness contract examples.

Compatible service copies

SlackService and SlackChannel support compatible-copy class injection; Slack channel tokens remain isolated.