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.
Maintainers
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-signalReact package (flags + provider/hooks):
npm install @fire-signal/react-sdk fire-signalpnpm 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
maxBatchSizeis reached orflushIntervalMselapses 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 shutdownYou 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'],
}
);
tagsandaudienceare different:
tags: route which URLs/providers in Fire-Signal are selectedaudience: sent only to Fire Platform (fire://) to filter channelsFire Platform also supports
segmentIdwhen you want explicit segment targeting. Fire Platform also supportstemplateKeyfor server-side template rendering.Note: currently Fire Platform
sendexpectsbodyin the payload. When usingtemplateKey, keep a concise fallbackbody(and optionaltitle).
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 bundlesfp_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 workspace404or DNS error: wrong host infire://...@<host>queuedbut no delivery: check channelaudiencelabels or segment filters in Fire Platform- no send result in mixed setups: check
tagsrouting in Fire-Signal (SendOptions.tags)
Fire Platform Deep Guide
Use this as the single mental model when sending through fire://:
tagsdecide which URLs/providers Fire-Signal will callaudienceandsegmentIddecide which Fire Platform channels receive the messagetemplateKeytells Fire Platform which server-side template to resolvemetadatacarries 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 inmetadata - Using direct provider URLs with placeholders (e.g.
mailto://...?to={email}): useoptions.params - Mixing both in one send is valid:
paramsresolves local URLs,metadatais sent to Fire Platform
Template behavior with templateKey:
- Fire Platform resolves the final message from the selected template
metadatamust include all required template variables- keep
bodyas 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 (
audienceorsegmentId) to keep intent explicit
Request Mapping (what Fire-Signal sends to Fire Platform)
When using fire://, Fire-Signal forwards:
title->titlebody->bodymetadata->dataactions->actionsaudience->audiencesegmentId->segmentIdtemplateKey->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:
- confirm
fire://<api_key>@<host>host and key are correct - verify
tagsactually include thefire://URL entry - verify
audiencelabels exist on channels (or usesegmentId) - if using templates, ensure
templateKeymatches template name exactly - verify required template variables exist in
metadata - 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"
fiShell/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 silentError 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
mainforfire:// - use one or more
monitoringfallback 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/webhookTokenQuery 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:
- Go to Discord → Server Settings → Integrations → Webhooks
- Create a webhook and copy the URL
- Extract
webhookIdandwebhookTokenfrom:https://discord.com/api/webhooks/{webhookId}/{webhookToken}
URL Format:
tgram://botToken/chatId
telegram://botToken/chatIdQuery 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:
- Create a bot with @BotFather, get the token
- Get your chat ID from @userinfobot
- For groups, use the negative chat ID (e.g.,
-1001234567890)
URL Format:
rocketchat://host/webhookToken
rocketchats://host/webhookToken # HTTPSQuery 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:
- Go to Rocket.Chat → Administration → Integrations → Incoming Webhooks
- Create a webhook and copy the token
- Use:
rocketchat://host/token
URL Format:
slack://T00000000/B00000000/XXXXXXXXXXXXXXXXQuery 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:
- Go to Slack → Apps → Incoming Webhooks
- Create a webhook for your workspace
- Extract
T.../B.../XXXfrom the webhook URL
URL Format:
mailto://user:[email protected][email protected]
mailtos://user:[email protected]:[email protected] # TLSQuery 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:
- Encode
@in username as%40 - Use
mailtos://for TLS (port 465 or 587) - For Gmail, enable "Less secure apps" or use App Passwords
URL Format:
json://api.example.com/webhook
jsons://api.example.com/webhook # HTTPSFeatures:
| 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:
tagscomes fromSendOptions.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 authQuery 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:
- Use
ntfy.shor self-host your own server - Subscribe to the topic using the ntfy app
- Use the topic name in the URL
URL Format:
gotify://my-server.com/appToken
gotifys://my-server.com/appToken # HTTPSQuery 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:
- Self-host Gotify or use a hosted instance
- Create an Application in Settings → Applications
- Copy the app token
URL Format:
gchat://SPACE_ID/KEY/TOKENFeatures:
| 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:
- Open a Google Chat space → Settings → Apps & Integrations → Webhooks
- Create a webhook and copy the URL:
https://chat.googleapis.com/v1/spaces/SPACE/messages?key=KEY&token=TOKEN - Convert to:
gchat://SPACE/KEY/TOKEN(URL-encode slashes as%2F)
URL Format:
mmost://host/HOOK_ID
mmosts://host/HOOK_ID # HTTPSQuery 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:
- Go to Mattermost → Integrations → Incoming Webhooks
- Create a webhook and copy the hook ID
- Use:
mmost://host/HOOK_ID
URL Format:
msteams://tenant.webhook.office.com/webhookb2/GUID@GUID/IncomingWebhook/GUID/GUIDQuery 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:
- Go to MS Teams → Channel settings → Connectors → Incoming Webhook
- Create a webhook and copy the URL
- Replace
https://withmsteams://
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:+1ToWhatsAppPath 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:
- Get credentials from: Twilio Console → Account Info
- Use your Twilio phone number as
@+1FromPhone - 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=productionawait 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 sendingExamples:
# 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-cito see real build notifications.See
.github/workflows/ci.ymlfor 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 buildManual 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_HOSTandFS_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.
