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

@rojan404/telegram-alerts

v0.1.1

Published

Telegram alerts for SaaS founders: one message on every signup, subscription, cancellation or payment. Zero dependencies, never throws.

Readme

@rojan404/telegram-alerts

Telegram alerts for SaaS founders. One message on your phone for every signup, subscription, payment or cancellation. Zero dependencies, ESM + CJS, typed, and it never throws: a Telegram outage can never break a sign-in or a billing webhook.

Dingcut - New subscription
Name: Ada Lovelace
Email: [email protected]
Plan: Pro
Interval: annual
Amount: 14.99 USD

Install

npm install @rojan404/telegram-alerts

Node 18+ (uses the built-in fetch).

Setup (2 minutes)

  1. In Telegram, open @BotFather, send /newbot, copy the token.

  2. Send your new bot any message (or add it to a group you want alerts in).

  3. Find the chat id and send a test:

    TELEGRAM_BOT_TOKEN=123:abc npx @rojan404/telegram-alerts setup --product "Dingcut"

    It prints the chat id and sends Dingcut - Test alert.

  4. Set both variables in your app (Vercel, .env, etc.):

    TELEGRAM_BOT_TOKEN=123:abc
    TELEGRAM_CHAT_ID=987654321

The CLI also reads ./.env, so from a project directory npx @rojan404/telegram-alerts setup is enough.

Usage

Create one client per product and export it:

// lib/alerts.ts
import { createTelegramAlerts } from '@rojan404/telegram-alerts';

export const alerts = createTelegramAlerts({ product: 'Dingcut' });
// botToken and chatId default to TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID, read at send time.

Then call it wherever the event happens:

await alerts.signup({ name: user.name, email: user.email, provider: 'google' });

await alerts.subscription({
  email,
  plan: 'Pro',
  interval: 'annual',
  amount: 14.99,
  currency: 'usd',
});

await alerts.payment({ email, amount: 14.99, currency: 'usd', reference: invoice.id });

await alerts.cancellation({ email, plan: 'Pro', reason: 'too expensive', endsAt: periodEnd });

// Anything else
await alerts.event('Refund issued', { email, amount: '14.99 USD' });

// Raw text
await alerts.send('Dingcut - deploy finished');

Every method resolves to a SendResult:

{ ok: true, chatId: '987654321' }
{ ok: false, reason: 'unconfigured' | 'timeout' | 'http' | 'network', status?, error? }

Unconfigured (no token or chat id) is a silent no-op, so the same code ships to dev, preview and production. Every other failure is logged and returned; nothing throws.

Fields

Known fields render first in a fixed order, then any extra keys you pass, in insertion order. Empty values (null, undefined, '') are skipped, so you never see Email: -. Keys are humanized: licenseId becomes License id. Booleans render as yes/no, dates as ISO strings.

amount is in major units. Stripe gives cents, so pass amount_total / 100.

Auth.js / NextAuth

createUser fires once per account (unlike signIn, which fires on every visit), so it is the right signup hook:

import NextAuth from 'next-auth';
import { alerts } from '@/lib/alerts';

export const { handlers, auth } = NextAuth({
  // ...
  events: {
    ...alerts.authEvents(),
    // or combine with your own:
    // async createUser({ user }) { await createWorkspace(user); await alerts.signup(user); },
  },
});

Stripe webhook

case 'checkout.session.completed': {
  const s = event.data.object;
  await alerts.subscription({
    email: s.customer_details?.email,
    plan: s.metadata?.plan,
    amount: (s.amount_total ?? 0) / 100,
    currency: s.currency,
    reference: s.subscription,
  });
  break;
}
case 'customer.subscription.deleted': {
  const sub = event.data.object;
  await alerts.cancellation({ plan: sub.items.data[0]?.price.nickname, endsAt: new Date(sub.ended_at! * 1000) });
  break;
}

Freemius

Alert only when the entitlement row is new, since the checkout redirect and the webhook both deliver the same license:

const existing = await prisma.entitlement.findUnique({ where: { licenseId } });
await prisma.entitlement.upsert(/* ... */);
if (!existing && purchase.isActive) {
  await alerts.subscription({
    email: purchase.email,
    plan: 'Pro',
    interval: purchase.billingCycle,
    amount: purchase.initialAmount,
    currency: purchase.currency,
    licenseId,
  });
}

Dedupe

The package has no storage, so it cannot know whether it already told you about a purchase. Gate the call on your own "is this new" check, as in the Freemius example.

Serverless

await the call. Fire-and-forget promises can be killed when a Vercel or Lambda function returns. The default 5s timeout bounds the cost.

Options

createTelegramAlerts({
  product: 'Dingcut', // required, message prefix
  botToken: '...', // default: process.env.TELEGRAM_BOT_TOKEN
  chatId: '1,2' | ['1', '2'], // default: process.env.TELEGRAM_CHAT_ID; several ids fan out
  timeoutMs: 5000,
  silent: false, // Telegram disable_notification
  logger: console | false, // where failures are logged
  onError: (result) => {}, // metrics hook, called after logging
  fetch: customFetch, // for tests or proxies
  apiBase: 'https://api.telegram.org',
});

alerts.enabled tells you whether a token and chat id currently resolve. alerts.format.* are the pure formatters, useful for previews and tests.

CLI

telegram-alerts setup   [--product X] [--token T] [--chat C] [--env path]
telegram-alerts chat-id
telegram-alerts test [text...]

Publishing (maintainers)

npm version patch|minor|major   # bumps, tags
git push --follow-tags
npm publish                     # prepublishOnly runs typecheck, lint, tests, build

Or publish a GitHub release; .github/workflows/publish.yml publishes with provenance using the NPM_TOKEN secret.

License

MIT