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

fire-signal

v0.5.1

Published

One call. Every channel. Zero hassle. Send notifications to Discord, Rocket.Chat, Slack, Telegram, Email, WhatsApp, SMS and more.

Readme


💡 The Problem

You're building an app and need to notify your team when:

  • A deploy completes
  • A payment fails
  • A user signs up
  • An error occurs in production

Some team members prefer Discord. Others use Rocket.Chat. Some like Slack. Your ops team needs email. And you want real-time alerts on Telegram.

Without Fire-Signal: You maintain 4+ different integrations, each with their own API, error handling, and configuration.

With Fire-Signal: One line of code. All channels. Done.


⚡ Quick Example

import { FireSignal } from 'fire-signal';

const fire = new FireSignal({
  urls: [
    'fire://fp_live_your_api_key',
    'discord://webhookId/webhookToken',
    'tgram://botToken/chatId',
    'rocketchat://chat.company.com/webhookToken',
    'slack://T00000/B00000/XXXXXX',
    'mailto://user:[email protected][email protected]',
  ],
});

// One call. Every channel.
await fire.send({
  title: '🚀 Deploy Successful',
  body: 'Production updated to v2.1.0',
});

SDK Docs

  • React SDK complete docs: https://github.com/fire-signal/fire-signal-react-sdk#readme

📦 Installation

npm install fire-signal

React package (flags + provider/hooks):

npm install @fire-signal/react-sdk fire-signal
pnpm add fire-signal
yarn add fire-signal

🔥 Fire Platform (fire://) Quick Start

Use Fire-Signal as a thin client to publish messages to Fire Platform.

import { FireSignal } from 'fire-signal';

const fire = new FireSignal();
fire.add('fire://fp_live_your_api_key');

await fire.send({
  title: 'Deploy Successful',
  body: 'Version 2.3.0 is live',
});

Platform Runtime APIs (track, identify, incident, flags)

When fire:// is configured, Fire-Signal also enables platform runtime APIs:

import { FireSignal } from 'fire-signal';

const fire = new FireSignal();

// Host defaults to api.fire-signal.com
fire.add('fire://fp_live_your_api_key');

await fire.track('checkout.started', {
  user: { id: 'user_123' },
  properties: { plan: 'PLUS', value: 249.9 },
});

await fire.identify('user_123', {
  email: '[email protected]',
  plan: 'PLUS',
  locale: 'pt-BR',
});

await fire.incident.report({
  code: 'payment_gateway_timeout',
  fingerprint: 'checkout:payment:timeout',
  severity: 'P1',
  message: 'Provider timeout after 30s',
});

const promoCode = await fire.flags.getVariantValue<string>('checkout.promocode', {
  user: { id: 'user_123' },
  traits: { plan: 'PLUS' },
});

Without fire://, these runtime APIs warn and no-op by default. Use strictPlatformProvider: true to throw instead.

Track batching and delivery guarantees

fire.track(...) uses in-memory batching by default.

  • automatic flush happens when maxBatchSize is reached or flushIntervalMs elapses
  • await fire.track(...) resolves only after that event is actually posted (or rejects on error)
  • await fire.flush() forces immediate drain of queued track events
  • queue is in-memory only (not persisted), so hard crashes/kill before flush can lose queued events

If you run short-lived jobs/workers, always await tracks and flush on shutdown:

const fire = new FireSignal({
  trackBatch: {
    enabled: true,
    flushIntervalMs: 1000,
    maxBatchSize: 50,
  },
});

await fire.track('checkout.started', { user: { id: 'user_123' } });
await fire.flush(); // call before graceful shutdown

You can also enable automatic best-effort flush on process exit signals:

const fire = new FireSignal({
  trackBatch: {
    enabled: true,
    autoFlushOnExit: true,
    flushOnExitTimeoutMs: 1500,
  },
});

If you need immediate per-call delivery (no queue), disable batching:

const fire = new FireSignal({
  trackBatch: { enabled: false },
});

More SaaS examples:

const fire = new FireSignal();
fire.add('fire://fp_live_your_api_key');

// 1) Basic incident broadcast
await fire.send({
  title: 'Incident Detected',
  body: 'Checkout latency above SLO',
});

// 2) Send only to channels labeled with audience
await fire.send(
  {
    title: 'Security Alert',
    body: 'Suspicious login detected',
  },
  { audience: ['security', 'oncall'] }
);

// 3) Include metadata for templates/channels in Fire Platform
await fire.send(
  {
    title: 'New Customer',
    body: 'Enterprise plan activated',
    metadata: {
      customerName: 'Acme Inc',
      accountManager: '[email protected]',
      mrr: 249,
    },
  },
  { audience: ['sales', 'csm'] }
);

// 4) Use tags to route inside Fire-Signal + audience for Platform filtering
// (useful when you mix fire:// with other providers in the same instance)
fire.add('slack://T00000/B00000/XXXXXX', ['backup']);

await fire.send(
  {
    title: 'Order Failed',
    body: 'Payment provider timeout',
    metadata: { orderId: 'ORD-8842', region: 'sa-east-1' },
  },
  {
    tags: ['platform'],
    audience: ['ops', 'payments'],
  }
);

// 5) Use a Fire Platform template by key
await fire.send(
  {
    title: 'Invoice update',
    body: 'Invoice status update', // kept as fallback/compat field
    metadata: {
      user: { name: 'Alice' },
      order: { id: 'ORD-1001', total: 249.9 },
    },
  },
  {
    templateKey: 'sales_welcome',
    audience: ['sales'],
  }
);

tags and audience are different:

  • tags: route which URLs/providers in Fire-Signal are selected
  • audience: sent only to Fire Platform (fire://) to filter channels

Fire Platform also supports segmentId when you want explicit segment targeting. Fire Platform also supports templateKey for server-side template rendering.

Note: currently Fire Platform send expects body in the payload. When using templateKey, keep a concise fallback body (and optional title).

fire:// URL format:

fire://<api_key>

Examples:

# Public SaaS (recommended)
fire://fp_live_your_api_key

# Frontend publishable key (restricted scopes)
fire://fp_pub_your_publishable_key

# Dedicated/self-hosted API host
fire://[email protected]

Key types:

  • fp_live_... (SECRET): server-side key, broader scopes, never expose in browser/mobile client bundles
  • fp_pub_... (PUBLISHABLE): restricted key for client-side usage (for example flag evaluation and safe runtime calls)

Common errors:

  • 401 Unauthorized: API key invalid/revoked or from another workspace
  • 404 or DNS error: wrong host in fire://...@<host>
  • queued but no delivery: check channel audience labels or segment filters in Fire Platform
  • no send result in mixed setups: check tags routing in Fire-Signal (SendOptions.tags)

Fire Platform Deep Guide

Use this as the single mental model when sending through fire://:

  • tags decide which URLs/providers Fire-Signal will call
  • audience and segmentId decide which Fire Platform channels receive the message
  • templateKey tells Fire Platform which server-side template to resolve
  • metadata carries template variables and provider-specific dynamic values

params vs metadata vs data

| Field | Where you set it | What it does | Forwarded to Fire Platform? | | --- | --- | --- | --- | | options.params | fire.send(message, { params }) | Replaces {placeholder} in URLs added with fire.add(...) | No | | message.metadata | fire.send({ ..., metadata }) | Message context for providers; in fire:// it becomes Fire Platform data | Yes (metadata -> data) | | data | Fire Platform REST (POST /v1/send) | Dynamic values for template variables and provider placeholders inside Platform | N/A (already in Platform API) |

Rules of thumb:

  • Using fire://: put dynamic values in metadata
  • Using direct provider URLs with placeholders (e.g. mailto://...?to={email}): use options.params
  • Mixing both in one send is valid: params resolves local URLs, metadata is sent to Fire Platform

Template behavior with templateKey:

  • Fire Platform resolves the final message from the selected template
  • metadata must include all required template variables
  • keep body as a compatibility fallback while API requires it

Option Semantics (tags vs audience vs segmentId vs templateKey)

| Field | Layer | Purpose | Typical value | | --- | --- | --- | --- | | tags | Fire-Signal | Select URLs/providers inside the current FireSignal instance | ['platform', 'backup'] | | audience | Fire Platform | Filter channels by channel audience labels | ['ops', 'oncall'] | | segmentId | Fire Platform | Explicit segment targeting | 'seg_01JX...' | | templateKey | Fire Platform | Resolve server-side template (templateKey === template name) | 'billing_invoice' |

Important targeting rule:

  • prefer one targeting strategy per send (audience or segmentId) to keep intent explicit

Request Mapping (what Fire-Signal sends to Fire Platform)

When using fire://, Fire-Signal forwards:

  • title -> title
  • body -> body
  • metadata -> data
  • actions -> actions
  • audience -> audience
  • segmentId -> segmentId
  • templateKey -> templateKey

This means template variables must be present in metadata:

await fire.send(
  {
    title: 'Invoice update',
    body: 'Invoice status update',
    metadata: {
      user: { name: 'Alice' },
      invoice: { id: 'INV-1001', total: 249.9 },
    },
  },
  {
    templateKey: 'billing_invoice',
    audience: ['billing'],
  }
);

Production Patterns

Template + audience:

await fire.send(
  {
    title: 'Invoice update',
    body: 'Fallback body',
    metadata: {
      customerName: 'Acme Inc',
      invoiceId: 'INV-1001',
      amount: 'R$ 249,90',
      dueDate: '2026-03-20',
    },
  },
  {
    templateKey: 'billing_invoice',
    audience: ['billing', 'finance'],
  }
);

Segment targeting:

await fire.send(
  {
    title: 'Deploy completed',
    body: 'Version 3.2.1 in production',
    metadata: { service: 'checkout', env: 'prod' },
  },
  {
    segmentId: 'seg_01JXABCDEF123456',
  }
);

Mixed providers with clean routing:

const fire = new FireSignal();
fire.add('fire://fp_live_your_api_key', ['platform']);
fire.add('discord://123/abc', ['fallback']);

await fire.send(
  {
    title: 'Incident',
    body: 'Checkout latency > 2s',
    metadata: { service: 'checkout', severity: 'high' },
  },
  {
    tags: ['platform'],
    audience: ['ops', 'oncall'],
  }
);

Debugging Checklist

If messages are accepted but not delivered as expected:

  1. confirm fire://<api_key>@<host> host and key are correct
  2. verify tags actually include the fire:// URL entry
  3. verify audience labels exist on channels (or use segmentId)
  4. if using templates, ensure templateKey matches template name exactly
  5. verify required template variables exist in metadata
  6. use Fire Platform message trace/logs to inspect per-channel failures

CLI equivalents:

# Template + audience
fire-signal -t "Invoice" -b "Fallback body" \
  --template-key billing_invoice \
  -a billing,finance \
  fire://fp_live_your_api_key

# Segment
fire-signal -t "Deploy" -b "v3.2.1 done" \
  --segment-id seg_01JXABCDEF123456 \
  fire://fp_live_your_api_key

✨ Features

| Feature | Description | | ------------------------------ | ---------------------------------------------------------- | | 📡 Multi-channel broadcast | Discord, Slack, Telegram, Email, Rocket.Chat, and webhooks | | 🔗 URL-based config | No complex setup — just fire://key | | 🏷️ Tag-based routing | Send to specific audiences with tags | | 📎 Attachments | Send files via Email, Discord, and Telegram | | 💻 CLI included | Integrate into shell scripts and CI/CD | | 📁 Config file support | Centralize channels in ~/.fire-signal.yml | | ⚡ TypeScript native | Full type safety and autocomplete | | 🔧 Extensible | Create custom providers in minutes | | 🪶 Lightweight | Minimal dependencies |


🎯 Use Cases

Welcome Email for New Users

import { FireSignal } from 'fire-signal';

// Configure once with placeholder
const fire = new FireSignal();
fire.add('fire://fp_live_your_api_key', ['platform']);
fire.add('mailto://noreply%40myapp.com:[email protected]?to={email}', [
  'email',
]);

async function onUserSignup(user: User) {
  await fire.send(
    {
      title: 'Welcome to MyApp!',
      body: `Hi ${user.name}, thanks for signing up.`,
      metadata: {
        userId: user.id,
        email: user.email,
      },
    },
    {
      tags: ['platform', 'email'],
      audience: ['customers'],
      params: { email: user.email },
    }
  );
}

Multi-Team Notification System

Configure different channels for different teams using tags and placeholders:

import { FireSignal } from 'fire-signal';

// Single instance, multiple audiences
const fire = new FireSignal();

fire.add('fire://fp_live_your_api_key', [
  'sales',
  'dev',
  'ops',
  'email',
]);

// Fixed channels (no placeholders)
fire.add('discord://sales-webhook/token', ['sales']);
fire.add('slack://T.../B.../XXX', ['sales', 'management']);
fire.add('tgram://bot/dev-chat', ['dev']);
fire.add('rocketchat://chat.company.com/webhook', ['dev', 'ops']);

// Dynamic channel (with placeholder)
fire.add('mailto://alerts%40company.com:[email protected]?to={email}', [
  'email',
]);

// New sale? Notify sales team (fixed)
await fire.send(
  { title: '💰 New Sale', body: 'Order #1234 - $599.00' },
  { tags: ['sales'] }
);

// Deploy complete? Notify dev team (fixed)
await fire.send(
  { title: '🚀 Deployed', body: 'v2.1.0 is live on production' },
  { tags: ['dev'] }
);

// Email specific user (dynamic)
await fire.send(
  { title: '🎫 Ticket Update', body: 'Your ticket has been resolved.' },
  { tags: ['email'], params: { email: '[email protected]' } }
);

E-commerce Order Flow

import { FireSignal } from 'fire-signal';

const fire = new FireSignal();

fire.add('fire://fp_live_your_api_key', [
  'warehouse',
  'finance',
  'support',
  'customer',
]);

// Internal channels (fixed)
fire.add('rocketchat://chat.company.com/webhookToken', ['warehouse']);
fire.add('discord://finance-webhook/token', ['finance']);
fire.add('tgram://bot/support-chat', ['support']);

// Customer emails (dynamic placeholder)
fire.add(
  'mailto://orders%40store.com:[email protected]?to={customer_email}',
  ['customer']
);

// Order placed
async function onOrderCreated(order: Order, customer: Customer) {
  // Email customer (uses placeholder)
  await fire.send(
    {
      title: 'Order Confirmed',
      body: `Order #${order.id} - Total: $${order.total}`,
    },
    { tags: ['customer'], params: { customer_email: customer.email } }
  );

  // Notify warehouse (fixed channel)
  await fire.send(
    {
      title: '📦 New Order',
      body: `#${order.id} - ${order.items.length} items`,
    },
    { tags: ['warehouse'] }
  );
}

// Payment received
async function onPaymentReceived(payment: Payment) {
  await fire.send(
    {
      title: '💳 Payment',
      body: `$${payment.amount} for order #${payment.orderId}`,
    },
    { tags: ['finance'] }
  );
}

CI/CD Pipeline Notifications

Use the CLI directly in your pipelines:

GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh

      - name: Notify Success
        if: success()
        run: |
          npx fire-signal \
            -t "✅ Deploy Successful" \
            -b "Commit: ${{ github.sha }}" \
            "${{ secrets.DISCORD_WEBHOOK }}"

      - name: Notify Failure
        if: failure()
        run: |
          npx fire-signal \
            -t "❌ Deploy Failed" \
            -b "Check: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
            "${{ secrets.DISCORD_WEBHOOK }}"

GitLab CI:

# .gitlab-ci.yml
deploy:
  stage: deploy
  script:
    - ./deploy.sh
  after_script:
    - |
      if [ "$CI_JOB_STATUS" = "success" ]; then
        npx fire-signal -t "✅ Deploy OK" -b "Pipeline #$CI_PIPELINE_ID" "$DISCORD_WEBHOOK"
      else
        npx fire-signal -t "❌ Deploy Failed" -b "Check $CI_PIPELINE_URL" "$DISCORD_WEBHOOK"
      fi

Shell/Cron:

npx fire-signal -t "🔄 Backup" -b "$(date)" "$NTFY_URL"
import { FireSignal } from 'fire-signal';

const fire = new FireSignal();
fire.add('fire://fp_live_your_api_key', [
  'build',
  'release',
]);
fire.add('discord://devops-webhook/token', ['build']);
fire.add('tgram://bot/releases-channel', ['release']);

await fire.send(
  { title: 'Build Started', body: `Branch: ${branch}` },
  { tags: ['build'] }
);
await fire.send(
  { title: '🚀 Released', body: `v${version} deployed` },
  { tags: ['release'] }
);
import { FireSignal } from 'fire-signal';

// Configure once at app startup
const fire = new FireSignal();

fire.add('fire://fp_live_your_api_key', [
  'user',
  'engineering',
  'sales',
  'support',
]);

// User notifications (transactional emails)
fire.add('mailto://noreply%40saas.com:[email protected]?to={user_email}', [
  'user',
]);

// Internal team notifications
fire.add('rocketchat://chat.internal.com/webhook', ['engineering']);
fire.add('discord://sales-webhook/token', ['sales']);
fire.add('slack://T.../B.../support', ['support']);

// Usage throughout the app:

// User signed up
await fire.send(
  { title: 'Welcome!', body: 'Your 14-day trial has started.' },
  { tags: ['user'] }
);
await fire.send(
  { title: '👤 New Signup', body: `${user.email} from ${user.company}` },
  { tags: ['sales'] }
);

// User upgraded to paid
await fire.send(
  { title: 'Thank you!', body: 'Your subscription is now active.' },
  { tags: ['user'] }
);
await fire.send(
  { title: '🎉 New Customer', body: `${user.company} - $${plan.price}/mo` },
  { tags: ['sales'] }
);

// User requested support
await fire.send(
  { title: '🎫 Support Ticket', body: `${ticket.subject} from ${user.email}` },
  { tags: ['support'] }
);

// Error in production
await fire.send(
  { title: '🐛 Error', body: `${error.message}\n${error.stack}` },
  { tags: ['engineering'] }
);

For professional emails with images and styling, use template engines like Handlebars or MJML:

// templates/order-confirmation.ts
export const orderTemplate = `
<!DOCTYPE html>
<html>
<head>
  <style>
    .container { max-width: 600px; margin: 0 auto; font-family: Arial; }
    .header { background: #4F46E5; color: white; padding: 20px; text-align: center; }
    .button { background: #4F46E5; color: white; padding: 12px 24px; 
              text-decoration: none; border-radius: 4px; display: inline-block; }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <img src="https://mycompany.com/logo.png" alt="Logo" width="150" />
    </div>
    <div style="padding: 20px;">
      <h1>Olá {{name}}!</h1>
      <p>Seu pedido <strong>#{{orderId}}</strong> foi confirmado.</p>
      <p>Total: <strong>R$ {{total}}</strong></p>
      <p style="text-align: center; margin-top: 20px;">
        <a class="button" href="https://mycompany.com/pedido/{{orderId}}">Ver Pedido</a>
      </p>
    </div>
  </div>
</body>
</html>
`;
import Handlebars from 'handlebars';
import { orderTemplate } from './templates/order-confirmation';
import { FireSignal } from 'fire-signal';

const fire = new FireSignal();
fire.add('fire://fp_live_your_api_key', ['customer']);
fire.add('mailto://orders%40company.com:[email protected]?to={email}', [
  'customer',
]);

// Compile template with data
const html = Handlebars.compile(orderTemplate)({
  name: 'João Silva',
  orderId: '12345',
  total: '299,90',
});

// Send rich HTML email
await fire.send(
  { title: 'Pedido Confirmado', body: html },
  { tags: ['customer'], params: { email: customer.email } }
);

💡 Tip: Images must use public URLs (https://...) or base64 data URLs. Local file paths don't work in emails.

💡 Tip: For responsive emails, consider using MJML which compiles to email-safe HTML.


🏗️ Framework Integration

NestJS

// fire.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { FireSignal } from 'fire-signal';

@Injectable()
export class FireService implements OnModuleInit {
  private fire: FireSignal;

  onModuleInit() {
    this.fire = new FireSignal();

    // Fire Platform first
    this.fire.add(process.env.FIRE_PLATFORM_URL!, ['user', 'team', 'critical']);

    // User notifications
    this.fire.add(process.env.SMTP_URL, ['user']);

    // Team notifications
    this.fire.add(process.env.DISCORD_WEBHOOK, ['team']);
    this.fire.add(process.env.SLACK_WEBHOOK, ['team', 'sales']);
    this.fire.add(process.env.TELEGRAM_BOT_URL, ['critical']);
  }

  async notifyUser(email: string, title: string, body: string) {
    await this.fire.send({ title, body }, { tags: ['user'] });
  }

  async notifyTeam(title: string, body: string) {
    await this.fire.send({ title, body }, { tags: ['team'] });
  }

  async alertCritical(message: string) {
    await this.fire.send(
      { title: '🚨 CRITICAL', body: message },
      { tags: ['critical'] }
    );
  }
}

Next.js (App Router)

// lib/fire.ts
import { FireSignal } from 'fire-signal';

export const fire = new FireSignal();

// Configure channels
fire.add(process.env.FIRE_PLATFORM_URL!, ['internal', 'sales', 'user']);
fire.add(process.env.DISCORD_WEBHOOK!, ['internal']);
fire.add(process.env.ROCKET_CHAT_WEBHOOK!, ['internal', 'sales']);
fire.add(process.env.SMTP_URL!, ['user']);
// app/api/webhooks/stripe/route.ts
import { fire } from '@/lib/fire';

export async function POST(req: Request) {
  const event = await req.json();

  if (event.type === 'payment_intent.succeeded') {
    // Notify sales team
    await fire.send(
      {
        title: '💰 Payment Received',
        body: `$${event.data.object.amount / 100} from ${event.data.object.customer}`,
      },
      { tags: ['sales'] }
    );
  }

  if (event.type === 'payment_intent.failed') {
    // Alert on all internal channels
    await fire.send(
      {
        title: '❌ Payment Failed',
        body: `Customer: ${event.data.object.customer}`,
      },
      { tags: ['internal'] }
    );
  }

  return Response.json({ received: true });
}

Express

// lib/fire.ts
import { FireSignal } from 'fire-signal';

export const fire = new FireSignal();

fire.add(process.env.DISCORD_WEBHOOK, ['dev']);
fire.add(process.env.SLACK_WEBHOOK, ['business']);
fire.add(process.env.SMTP_URL, ['user']);
// routes/users.ts
import { fire } from '../lib/fire';

router.post('/signup', async (req, res) => {
  const user = await createUser(req.body);

  // Welcome email to user
  await fire.send(
    { title: 'Welcome!', body: `Hi ${user.name}, your account is ready.` },
    { tags: ['user'] }
  );

  // Notify business team
  await fire.send(
    { title: '👤 New User', body: `${user.name} (${user.email})` },
    { tags: ['business'] }
  );

  res.json(user);
});

Node.js Scripts / CI/CD

# After deploy
fire-signal -t "✅ Deploy Complete" -b "v2.0.0 deployed" discord://webhook/token

# Filter by tags from config
fire-signal -t "🚨 Alert" -b "Check logs" -g critical

📎 Attachments

Send files with your notifications (supported by Email, Discord, and Telegram):

import { readFileSync } from 'fs';
import { FireSignal } from 'fire-signal';

const fire = new FireSignal({
  urls: [
    'fire://fp_live_your_api_key',
    'mailto://user:[email protected][email protected]',
    'discord://webhookId/webhookToken',
    'tgram://botToken/chatId',
  ],
});

// Attach a file from URL
await fire.send({
  title: 'Monthly Report',
  body: 'Please find the attached report.',
  attachments: [{ url: 'https://example.com/report.pdf', name: 'report.pdf' }],
});

// Attach a local file (Buffer)
await fire.send({
  title: 'Invoice',
  body: 'Your invoice is attached.',
  attachments: [
    {
      content: readFileSync('./invoice.pdf'),
      name: 'invoice.pdf',
      contentType: 'application/pdf',
    },
  ],
});

// Multiple attachments
await fire.send({
  title: 'Project Files',
  body: 'Here are the files.',
  attachments: [
    { content: readFileSync('./doc.pdf'), name: 'doc.pdf' },
    { url: 'https://example.com/image.png', name: 'image.png' },
  ],
});

Note: Attachments are supported by Email, Discord, and Telegram. Other providers may support URL references only.


📡 Supported Providers

| Provider | Scheme | Auth | Attachments | Formatting | Limitations | Example | | --------------- | --------------------------- | ------------ | ----------- | -------------- | --------------- | -------------------------------- | | Fire Platform | fire:// | API Key | ❌ | Managed by Platform | Requires Fire Platform account | fire://fp_live_your_api_key | | Discord | discord:// | Webhook URL | ✅ Full | Markdown | 2000 char limit | discord://webhookId/token | | Telegram | tgram:// telegram:// | Bot Token | ✅ Full | Markdown/HTML | 4096 char limit | tgram://botToken/chatId | | Rocket.Chat | rocketchat:// rocket:// | Webhook | ❌ | Markdown | - | rocketchat://host/token | | Slack | slack:// | Webhook | ❌ | Markdown | - | slack://T.../B.../XXX | | Email | mailto:// mailtos:// | SMTP | ✅ Full | HTML | Encode @ as %40 | mailto://user:pass@smtp... | | Webhook | json:// jsons:// | Optional | ❌ | JSON | - | json://api.example.com/hook | | ntfy | ntfy:// ntfys:// | Optional | URL only | Plain | - | ntfy://ntfy.sh/topic | | Gotify | gotify:// gotifys:// | App Token | ❌ | Markdown | - | gotify://host/token | | Google Chat | gchat:// googlechat:// | Webhook | ❌ | Simple HTML | - | gchat://SPACE/KEY/TOKEN | | Mattermost | mmost:// mmosts:// | Webhook | ❌ | Markdown | - | mmost://host/HOOK_ID | | MS Teams | msteams:// | Webhook | ❌ | Adaptive Cards | - | msteams://tenant.webhook... | | OneSignal | onesignal:// | API Key | ❌ | Plain | - | onesignal://APP@KEY/ | | Pushover | pover:// pushover:// | User+API Key | ❌ | HTML optional | - | pover://USER@TOKEN/ | | Twilio | twilio:// | Account SID | ❌ | Plain (SMS) | - | twilio://SID:Token@+1.../+1... |

Legend:

  • ✅ Full = Supports file attachments (Buffer or URL)
  • ❌ = No attachment support via webhook
  • URL only = Attachments via URL reference only

🔧 Query Parameters

Query parameters allow you to customize notification behavior per-provider. They can be used in two ways:

Static Values

// Pushover with high priority and custom sound
fire.add('pover://user@token/?priority=1&sound=cosmic');

// ntfy with priority and tags
fire.add('ntfy://ntfy.sh/alerts?priority=high&tags=warning,server');

// Gotify with priority level
fire.add('gotifys://gotify.example.com/token?priority=8');

Dynamic Placeholders

Use {key} placeholders that get replaced by values from options.params:

fire.add('ntfy://ntfy.sh/alerts?tags={severity}');
fire.add('pover://user@token/?sound={alert_sound}');

await fire.send(
  {
    title: 'Server Alert',
    body: 'CPU usage exceeded 90%',
  },
  {
    params: {
      severity: 'urgent,server',
      alert_sound: 'siren',
    },
  }
);

When sending through fire://, put these dynamic values in metadata instead, because Fire Provider forwards metadata to Fire Platform as data.

Placeholders work in any part of the URL: path segments, query param values, etc.


📋 Logging & Error Handling

Log Levels

const fire = new FireSignal({
  logLevel: 'info', // 'silent' | 'error' | 'warn' | 'info' | 'debug'
});

CLI:

fire-signal --log-level debug -t "Test" -b "With debug output" "ntfy://..."
fire-signal -v ...  # Same as --log-level debug
fire-signal -q ...  # Same as --log-level silent

Error Fallback (onError)

Automatically notify a fallback channel when a provider fails:

const fire = new FireSignal({
  onError: {
    fallbackTags: ['monitoring'], // Required: tags to send error to
    // Optional: custom message format
    // message: (error, ctx) => `Custom: ${error.message}`,
    // Optional: external callback (e.g., Sentry)
    // callback: async (error, ctx) => await sentry.captureException(error),
  },
});

fire.add('fire://fp_live_your_api_key', ['main']);
fire.add('tgram://bot/monitoring-chat', ['monitoring']);

// If fire:// fails, Telegram receives the fallback error notification
await fire.send(
  { title: 'Alert', body: 'Message' },
  { tags: ['main'], audience: ['ops', 'oncall'] }
);

Recommended pattern with Fire Platform primary:

  • use one entry tagged as main for fire://
  • use one or more monitoring fallback channels outside Fire Platform
  • keep fallback message short and operational (provider, reason, destination)

Error Messages: Fire-Signal provides human-readable HTTP error messages:

[404] Not Found - Webhook URL is invalid or deleted
[401] Unauthorized - Token expired or invalid credentials
[504] Gateway Timeout - Server did not respond in time

⏱️ Timeouts & Retries

# Custom timeout (10 seconds instead of default 30s)
fire-signal --timeout 10000 -t "Quick" -b "Message" ntfy://...

# Retry on failures (3 retries with exponential backoff)
fire-signal --retries 3 -t "Important" -b "Message" discord://...

With --retries 3 and default settings:

  • 1st retry: waits 1 second
  • 2nd retry: waits 2 seconds
  • 3rd retry: waits 4 seconds

Defaults: timeout: 30s, retryDelay: 1s, backoffMultiplier: 2, maxDelay: 30s

Retryable HTTP codes: 429, 500, 502, 503, 504


📦 Providers

URL Format:

discord://webhookId/webhookToken

Query Parameters:

| Parameter | Description | | ------------ | --------------------------------- | | username | Override webhook username | | avatar_url | Override webhook avatar | | tts | Text-to-speech (true / false) |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | --------------------------------------- | | Title | ✅ | Displayed as bold text | | Attachments | ✅ | Files upload via multipart | | Actions | ✅ | Rendered as clickable links in an Embed |

Example:

fire.add('discord://123456789/AbCdEfGhI?username=Deploy%20Bot');

await fire.send({
  title: 'Deployment Complete',
  body: 'Version 2.0.1 deployed to production.',
  actions: [
    { label: 'View Logs', url: 'https://logs.example.com' },
    {
      label: 'Rollback',
      url: 'https://deploy.example.com/rollback',
      style: 'danger',
    },
  ],
});

Setup:

  1. Go to Discord → Server Settings → Integrations → Webhooks
  2. Create a webhook and copy the URL
  3. Extract webhookId and webhookToken from: https://discord.com/api/webhooks/{webhookId}/{webhookToken}

URL Format:

tgram://botToken/chatId
telegram://botToken/chatId

Query Parameters:

| Parameter | Description | | -------------------------- | ---------------------------------------- | | parse_mode | HTML, Markdown, or MarkdownV2 | | disable_web_page_preview | Disable link previews (true / false) | | disable_notification | Send silently (true / false) |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------------- | | Title | ✅ | Displayed as bold text (Markdown) | | Attachments | ✅ | Sent via sendDocument API | | Actions | ✅ | Rendered as Inline Keyboard buttons |

Example:

fire.add('tgram://123456:ABC-DEF/987654321?parse_mode=Markdown');

await fire.send({
  title: 'Order Shipped',
  body: 'Your order #12345 has been shipped!',
  actions: [{ label: 'Track Order', url: 'https://track.example.com/12345' }],
});

Setup:

  1. Create a bot with @BotFather, get the token
  2. Get your chat ID from @userinfobot
  3. For groups, use the negative chat ID (e.g., -1001234567890)

URL Format:

rocketchat://host/webhookToken
rocketchats://host/webhookToken   # HTTPS

Query Parameters:

| Parameter | Description | | ---------- | ----------------------------- | | channel | Override channel (#general) | | username | Override bot username | | avatar | Override bot avatar URL |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | -------------------------- | | Title | ✅ | Displayed as bold text | | Attachments | ❌ | Not supported via webhooks | | Actions | ❌ | Not supported via webhooks |

Example:

fire.add('rocketchats://chat.company.com/abc123?channel=%23devops');

await fire.send({
  title: 'Build Complete',
  body: 'Frontend build succeeded.',
});

Setup:

  1. Go to Rocket.Chat → Administration → Integrations → Incoming Webhooks
  2. Create a webhook and copy the token
  3. Use: rocketchat://host/token

URL Format:

slack://T00000000/B00000000/XXXXXXXXXXXXXXXX

Query Parameters:

| Parameter | Description | | ------------ | ----------------------------- | | channel | Override channel (#general) | | username | Override bot username | | icon_emoji | Emoji icon (e.g., :robot:) | | icon_url | URL to icon image |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------------- | | Title | ✅ | Displayed as bold text | | Attachments | ❌ | Not supported via Incoming Webhooks | | Actions | ✅ | Rendered as Block Kit buttons |

Example:

fire.add('slack://T12345678/B87654321/xyzABC123?channel=%23alerts');

await fire.send({
  title: 'New Support Ticket',
  body: 'Ticket #5678 requires attention.',
  actions: [
    {
      label: 'View Ticket',
      url: 'https://support.example.com/5678',
      style: 'primary',
    },
    {
      label: 'Ignore',
      url: 'https://support.example.com/5678/close',
      style: 'danger',
    },
  ],
});

Setup:

  1. Go to Slack → Apps → Incoming Webhooks
  2. Create a webhook for your workspace
  3. Extract T.../B.../XXX from the webhook URL

URL Format:

mailto://user:[email protected][email protected]
mailtos://user:[email protected]:[email protected]   # TLS

Query Parameters:

| Parameter | Description | | --------- | ----------------------------------- | | to | Recipient email(s), comma-separated | | cc | CC recipient(s) | | bcc | BCC recipient(s) | | from | Override sender name | | name | Display name for sender |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ------------------------- | | Title | ✅ | Used as email subject | | Attachments | ✅ | Sent as email attachments | | Actions | ❌ | Not supported |

Example:

fire.add(
  'mailtos://alerts%40company.com:[email protected]:[email protected]'
);

await fire.send({
  title: 'Weekly Report',
  body: '<h1>Sales Report</h1><p>Revenue increased by 15%.</p>',
  attachments: [
    { name: 'report.pdf', url: 'https://reports.example.com/weekly.pdf' },
  ],
});

Setup:

  1. Encode @ in username as %40
  2. Use mailtos:// for TLS (port 465 or 587)
  3. For Gmail, enable "Less secure apps" or use App Passwords

URL Format:

json://api.example.com/webhook
jsons://api.example.com/webhook   # HTTPS

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ------------------------ | | Title | ✅ | Included in JSON payload | | Attachments | ❌ | Not supported | | Actions | ❌ | Not supported |

Payload Structure:

The webhook receives a JSON payload with the following structure:

{
  "title": "Message Title",
  "body": "Message body",
  "tags": ["tag1", "tag2"],
  "metadata": { "key": "value" }
}

Note: tags comes from SendOptions.tags, not from the message itself.

Example:

fire.add('jsons://api.example.com/notifications/webhook', ['alerts']);

await fire.send(
  {
    title: 'Custom Event',
    body: 'Something happened!',
    metadata: { eventId: 123, severity: 'high' },
  },
  { tags: ['alerts'] }
);
// Payload sent: { title: "Custom Event", body: "...", tags: ["alerts"], metadata: {...} }

URL Format:

ntfy://ntfy.sh/my-topic
ntfys://ntfy.sh/my-topic          # HTTPS
ntfy://user:[email protected]/topic # With auth

Query Parameters:

| Parameter | Description | | ---------- | ------------------------------------------------- | | priority | min, low, default, high, urgent | | tags | Comma-separated emoji tags (e.g., warning,fire) | | click | URL to open when notification is clicked | | attach | URL to attachment | | icon | Notification icon URL | | email | Email address for email notifications | | delay | Delay before sending (e.g., 30min, 2h) |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------------- | | Title | ✅ | Sent as notification title | | Attachments | ✅ | Via attach query param or content | | Actions | ❌ | Not supported via fire-signal |

Example:

fire.add('ntfys://ntfy.sh/my-alerts?priority=high&tags=warning');

await fire.send({
  title: 'Server Alert',
  body: 'CPU usage exceeded 90%.',
});

Setup:

  1. Use ntfy.sh or self-host your own server
  2. Subscribe to the topic using the ntfy app
  3. Use the topic name in the URL

URL Format:

gotify://my-server.com/appToken
gotifys://my-server.com/appToken   # HTTPS

Query Parameters:

| Parameter | Description | | ---------- | -------------------------- | | priority | 1-10, higher = more urgent |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | -------------------------- | | Title | ✅ | Sent as notification title | | Attachments | ❌ | Not supported | | Actions | ❌ | Not supported |

Example:

fire.add('gotifys://push.example.com/AbCdEfGh?priority=8');

await fire.send({
  title: 'Security Alert',
  body: 'Suspicious login detected.',
});

Setup:

  1. Self-host Gotify or use a hosted instance
  2. Create an Application in Settings → Applications
  3. Copy the app token

URL Format:

gchat://SPACE_ID/KEY/TOKEN

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------- | | Title | ✅ | Displayed as bold text | | Attachments | ❌ | Not supported via webhooks | | Actions | ❌ | Not supported via fire-signal |

Example:

fire.add('gchat://spaces%2FAAAA/keys%2Fbbbb/tokens%2Fcccc');

await fire.send({
  title: 'Calendar Reminder',
  body: 'Team standup in 15 minutes.',
});

Setup:

  1. Open a Google Chat space → Settings → Apps & Integrations → Webhooks
  2. Create a webhook and copy the URL: https://chat.googleapis.com/v1/spaces/SPACE/messages?key=KEY&token=TOKEN
  3. Convert to: gchat://SPACE/KEY/TOKEN (URL-encode slashes as %2F)

URL Format:

mmost://host/HOOK_ID
mmosts://host/HOOK_ID    # HTTPS

Query Parameters:

| Parameter | Description | | ---------- | -------------------------------------- | | channel | Override channel (#general, @user) | | username | Override webhook username | | icon_url | Override webhook icon |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------- | | Title | ✅ | Displayed as bold text | | Attachments | ❌ | Not supported via webhooks | | Actions | ❌ | Not supported via fire-signal |

Example:

fire.add('mmosts://chat.company.com/abc123def?channel=%23devops');

await fire.send({
  title: 'Deployment Notice',
  body: 'Backend v3.2.0 deployed to staging.',
});

Setup:

  1. Go to Mattermost → Integrations → Incoming Webhooks
  2. Create a webhook and copy the hook ID
  3. Use: mmost://host/HOOK_ID

URL Format:

msteams://tenant.webhook.office.com/webhookb2/GUID@GUID/IncomingWebhook/GUID/GUID

Query Parameters:

| Parameter | Description | | ------------- | ---------------------------------- | | theme_color | Hex color for card accent (no #) |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------------- | | Title | ✅ | Displayed as card title | | Attachments | ❌ | Not supported via Incoming Webhooks | | Actions | ✅ | Rendered as OpenUri buttons |

Example:

fire.add(
  'msteams://tenant.webhook.office.com/webhookb2/abc@def/IncomingWebhook/ghi/jkl?theme_color=FF0000'
);

await fire.send({
  title: 'Incident Alert',
  body: 'Service degradation detected in production.',
  actions: [
    { label: 'View Dashboard', url: 'https://grafana.example.com/d/prod' },
    {
      label: 'Acknowledge',
      url: 'https://incidents.example.com/123/ack',
      style: 'primary',
    },
  ],
});

Setup:

  1. Go to MS Teams → Channel settings → Connectors → Incoming Webhook
  2. Create a webhook and copy the URL
  3. Replace https:// with msteams://

URL Format:

onesignal://APP_ID@REST_API_KEY/
onesignal://APP_ID@REST_API_KEY/{player_id}/
onesignal://APP_ID@REST_API_KEY/#Segment%20Name/
onesignal://APP_ID@REST_API_KEY/@{external_user_id}/

Path Targets:

| Target | Description | | ------------- | ------------------------------------- | | {player_id} | Target by OneSignal player ID | | #{segment} | Target by segment (URL-encode spaces) | | @{user_id} | Target by external user ID | | {email} | Target by email address |

Query Parameters:

| Parameter | Description | | ---------- | ------------------------------------ | | subtitle | iOS subtitle | | language | 2-char language code (default: en) | | image | yes/no to include icon |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------- | | Title | ✅ | Sent as notification title | | Attachments | ❌ | Not supported | | Actions | ❌ | Not supported via fire-signal |

Example:

fire.add('onesignal://abc123@def456/#Active%20Users');

await fire.send({
  title: 'New Feature',
  body: 'Check out our new dashboard!',
});

Setup:

Get credentials from: OneSignal Dashboard → Settings → Keys & IDs

URL Format:

pover://USER_KEY@API_TOKEN/
pover://USER_KEY@API_TOKEN/{device}/

Path Targets:

| Target | Description | | ---------- | ------------------------- | | {device} | Target specific device(s) |

Query Parameters:

| Parameter | Description | | ---------- | ------------------------------ | | priority | -2 (lowest) to 2 (emergency) | | sound | Notification sound | | url | Supplementary URL | | html | yes/no for HTML formatting | | ttl | Time to live in seconds |

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------- | | Title | ✅ | Sent as notification title | | Attachments | ✅ | Image attachments supported | | Actions | ❌ | Not supported via fire-signal |

Example:

fire.add('pover://userKey123@appToken456?priority=1&sound=cosmic');

await fire.send({
  title: 'Urgent Alert',
  body: 'Server CPU at 100%!',
});

Setup:

Get credentials from: Pushover Dashboard

URL Format:

twilio://AccountSID:AuthToken@+1FromPhone/+1ToPhone
twilio://AccountSID:AuthToken@+1FromPhone/+1Phone1/+1Phone2
twilio://AccountSID:AuthToken@+1FromPhone/w:+1ToWhatsApp

Path Targets:

| Target | Description | | ------------ | --------------------------------- | | /+1Phone | SMS recipient (with country code) | | /w:+1Phone | WhatsApp recipient (w: prefix) |

Multiple recipients supported via path segments.

Features:

| Feature | Supported | Notes | | ----------- | :-------: | ----------------------------------- | | Title | ❌ | SMS/WhatsApp doesn't support titles | | Attachments | ✅ | MMS/WhatsApp media supported | | Actions | ❌ | Not supported |

Example:

fire.add('twilio://ACXXXX:authToken@+15551234567/+15559876543');

await fire.send({
  body: 'Your verification code is 123456.',
});

Setup:

  1. Get credentials from: Twilio Console → Account Info
  2. Use your Twilio phone number as @+1FromPhone
  3. Add recipient numbers as path segments

📁 Configuration File

Create ~/.fire-signal.yml:

urls:
  # Fire Platform first
  - url: 'fire://fp_live_your_api_key'
    tags: ['sales', 'management', 'dev', 'ops', 'critical']

  # Sales team
  - url: 'discord://sales-webhook/token'
    tags: ['sales']
  - url: 'slack://T.../B.../XXX'
    tags: ['sales', 'management']

  # Dev team
  - url: 'tgram://bot/dev-chat'
    tags: ['dev']
  - url: 'rocketchat://chat.company.com/webhook'
    tags: ['dev', 'ops']

  # Critical alerts (everyone)
  - url: 'mailto://alerts%40company.com:[email protected][email protected]'
    tags: ['critical']
const fire = new FireSignal();
await fire.loadConfig();

// Only sales team
await fire.send({ body: 'New lead!' }, { tags: ['sales'] });

// Only dev team
await fire.send({ body: 'Build passed' }, { tags: ['dev'] });

// Critical (email)
await fire.send({ body: 'Server down!' }, { tags: ['critical'] });

🌍 Environment Variables

# Space or comma separated URLs (fire:// first)
FIRE_SIGNAL_URLS="fire://fp_live_your_api_key discord://... tgram://... slack://..."

# Additional config file paths
FIRE_SIGNAL_CONFIG_PATH="/etc/fire-signal.yml:~/.fire-signal.yml"

🌍 Multi-Environment Support

Fire-Signal simplifies different configurations for development, staging, and production.

Mode Option

const fire = new FireSignal({
  mode: process.env.NODE_ENV === 'production' ? 'enabled' : 'dryRun',
});

| Mode | Behavior | | ------------ | -------------------------- | | 'enabled' | Normal operation (default) | | 'disabled' | Silent, skips all sends | | 'dryRun' | Logs but doesn't send |

Environment-Specific Config Files

Fire-Signal auto-detects fire-signal.{NODE_ENV}.yml:

fire-signal.yml             # Base (fallback)
fire-signal.development.yml # NODE_ENV=development
fire-signal.production.yml  # NODE_ENV=production
await fire.loadConfig(); // Auto-detects based on NODE_ENV

// Or load specific file:
await fire.loadConfig('config/staging.yml');

💻 CLI

fire-signal -t "Title" -b "Body" [urls...]

Options:
  -t, --title <title>      Notification title
  -b, --body <body>        Notification body (or pipe from stdin)
  -u, --url <url>          Provider URL (repeatable)
  -g, --tag <tags...>      Filter by tags
  --tags <tags>            Alias for -g/--tag
  -a, --audience <labels>  Fire Platform audience labels (comma or space separated)
  --template-key <key>     Fire Platform template key
  --segment-id <id>        Fire Platform segment ID
  -c, --config <paths...>  Additional config paths
  -v, --verbose            Debug output
  -q, --quiet              Errors only
  --dry-run                Show payload without sending
  --json                   Output results as JSON
  --timeout <ms>           Request timeout (default: 30000)
  --retries <n>            Retry attempts (default: 0)
  --validate               Validate URLs without sending

Examples:

# Direct (Fire Platform)
fire-signal -t "Deploy" -b "Done" fire://fp_live_your_api_key

# From env
export FIRE_SIGNAL_URLS="fire://fp_live_your_api_key discord://id/token tgram://bot/chat"
fire-signal -t "Alert" -b "Check logs"

# Pipe
echo "Build completed" | fire-signal -t "CI"

# Tags (from config)
fire-signal -t "Critical" -b "Error" -g critical

# Fire Platform audience filter
fire-signal -t "Security" -b "Suspicious login" \
  -a security,oncall \
  -u fire://fp_live_your_api_key

# Multiple providers with explicit URL flags
fire-signal -t "Deploy" -b "Done" \
  -u fire://fp_live_your_api_key \
  -u ntfy://ntfy.sh/deploy

# Fire Platform segment targeting
fire-signal -t "Release" -b "v3.2.0 shipped" \
  --segment-id seg_01JXABCDEF123456 \
  fire://fp_live_your_api_key

# Fire Platform template targeting
fire-signal -t "Invoice" -b "Fallback content" \
  --template-key billing_invoice \
  -a billing \
  fire://fp_live_your_api_key

# Dry run (preview without sending)
fire-signal --dry-run -t "Test" -b "Body" ntfy://ntfy.sh/test

# JSON output (for scripting)
fire-signal --json -t "Deploy" -b "Done" ntfy://ntfy.sh/test | jq .

# Validate URLs
fire-signal --validate 'ntfy://ntfy.sh/test' 'discord://123/abc'

🔧 Custom Providers

import { BaseProvider } from 'fire-signal';

class PagerDutyProvider extends BaseProvider {
  readonly id = 'pagerduty';
  readonly schemas = ['pagerduty'];

  parseUrl(raw: string) {
    // Parse pagerduty://routing-key
  }

  async send(message, ctx) {
    await fetch('https://events.pagerduty.com/v2/enqueue', {
      method: 'POST',
      body: JSON.stringify({
        routing_key: ctx.parsed.hostname,
        event_action: 'trigger',
        payload: { summary: message.body, severity: 'critical' },
      }),
    });
    return this.success();
  }
}

const fire = new FireSignal({
  providers: [new PagerDutyProvider()],
  urls: ['pagerduty://your-routing-key'],
});

📝 Templates Engine

Fire-Signal includes a built-in template engine powered by Handlebars. Templates separate message content from code, making it easy to manage and reuse notification formats.

Inline Templates

// Register an inline template
fire.registerTemplateInline('deploy', {
  title: 'Deploy: {{environment}}',
  body: 'Version {{version}} deployed by {{user}}.\n\nChanges:\n{{changes}}',
});

// Send using the template
await fire.sendTemplate('deploy', {
  environment: 'Production',
  version: 'v2.1.0',
  user: 'ci-bot',
  changes: '- Fixed login bug\n- Improved performance',
});

File-Based Templates

Templates can be stored in .hbs files with optional frontmatter for the title:

---
title: Alert: {{severity}}
---

**Service:**
{{service}}
**Status:**
{{status}}

{{#if details}}
  Details:
  {{details}}
{{/if}}
// Register from file
await fire.registerTemplate('alert', 'templates/alert.hbs');

// Send using the template
await fire.sendTemplate(
  'alert',
  {
    severity: 'HIGH',
    service: 'API Gateway',
    status: 'DOWN',
    details: 'Connection refused on port 443',
  },
  { tags: ['ops'] }
);

Template API

| Method | Description | | ------------------------------------------------ | ------------------------------------ | | registerTemplate(name, filePath) | Register template from a .hbs file | | registerTemplateInline(name, { title?, body }) | Register template inline | | sendTemplate(name, data, options?) | Render and send a template |


🚀 CI/CD Integration

Fire-Signal can be used in your CI/CD pipelines to notify on build success/failure.

GitHub Actions

- name: Notify Build Status
  env:
    FIRE_SIGNAL_URLS: ntfys://ntfy.sh/your-topic?priority=default
  run: |
    npx fire-signal \
      --title "Build Passed" \
      --body "Build succeeded on ${{ github.ref_name }}"

💡 Eating our own dog food: Fire-Signal uses itself in its own CI/CD pipeline!

Subscribe to ntfy.sh/fire-signal-ci to see real build notifications.

See .github/workflows/ci.yml for the implementation.


🛡️ Enterprise Resilience

Fire-Signal includes built-in rate limiting and circuit breaker patterns to prevent cascading failures and respect API limits.

Configuration

const fire = new FireSignal({
  resilience: {
    // Rate limiting: max 5 requests per second per provider
    rateLimit: {
      requests: 5,
      periodMs: 1000,
    },
    // Circuit breaker: open after 3 failures, reset after 30s
    circuitBreaker: {
      failureThreshold: 3,
      resetTimeoutMs: 30000,
    },
  },
});

Rate Limiting

The rate limiter uses a Token Bucket algorithm:

  • Each provider has its own bucket
  • Tokens are replenished over time
  • Requests are blocked when bucket is empty

Circuit Breaker

The circuit breaker protects against cascading failures:

| State | Description | | ----------- | ------------------------------------------------- | | CLOSED | Normal operation, requests allowed | | OPEN | Too many failures, requests blocked | | HALF_OPEN | Testing if service recovered, one request allowed |

import { CircuitState, ResilienceError } from 'fire-signal';

try {
  await fire.send({ body: 'Test' });
} catch (error) {
  if (error instanceof ResilienceError) {
    console.log(`Blocked: ${error.reason}`); // 'rate_limit' | 'circuit_breaker'
    console.log(`Retry after: ${error.retryAfterMs}ms`);
  }
}

📖 API Reference

const fire = new FireSignal({
  // Destinations to send to (e.g. ['fire://key', 'discord://webhook'])
  urls?: string[];

  // Custom logic (only if you created a custom provider class)
  // Note: Built-in providers (Discord, Slack, etc.) are loaded automatically.
  providers?: FSProvider[];

  skipDefaultProviders?: boolean;
  logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug';
  logger?: (message: string, level: string) => void;
  onError?: {
    fallbackTags?: string[];
    message?: (error: Error, context: FSErrorContext) => string;
    callback?: (error: Error, context: FSErrorContext) => void;
  };

  trackBatch?: {
    enabled?: boolean;
    flushIntervalMs?: number;
    maxBatchSize?: number;
    maxQueueSize?: number;
    autoFlushOnExit?: boolean;
    flushOnExitTimeoutMs?: number;
  };
});

fire.add(urls: string | string[], tags?: string[]);
await fire.loadConfig();
await fire.send(message: FSMessage, options?: SendOptions);
await fire.track(eventName: string, payload?: TrackPayload, options?: PlatformCallOptions);
await fire.flush();
await fire.dispose();
interface FSMessage {
  title?: string;
  body: string;
  attachments?: FSAttachment[];
  actions?: FSAction[];
  metadata?: Record<string, unknown>;
}

interface SendOptions {
  // Fire-Signal routing tags (selects URLs/providers)
  tags?: string[];

  // Fire Platform channel filter (only used by fire:// provider)
  audience?: string[];

  // Fire Platform segment targeting (only used by fire:// provider)
  segmentId?: string;

  // Fire Platform server-side template key (same value as template name)
  templateKey?: string;

  params?: Record<string, string>;
}

interface FSAction {
  label: string;
  url: string;
  style?: 'primary' | 'secondary' | 'danger';
}

interface FSAttachment {
  name: string;
  content?: Buffer;
  url?: string;
  contentType?: string;
}

✅ Release Checklist

Before release/publish, run this checklist in fire-signal:

pnpm typecheck
pnpm test:run
pnpm build

Manual checks:

  • README examples match current behavior (fire://, fp_live_*, fp_pub_*, track batching/flush)
  • React SDK docs are up to date (https://github.com/fire-signal/fire-signal-react-sdk#readme)
  • optional local contract validation with FS_E2E_HOST and FS_E2E_API_KEY
  • changelog/version bump is aligned with the release scope

🤝 Contributing

Contributions welcome! Please open an issue first to discuss what you'd like to change.


📄 License

MIT