GenerateSaaS

Admin Notifications

Push real-time operator alerts for signups, purchases, refunds, and cancellations to Discord, Slack, or Telegram with the @repo/admin-notifications package.

@repo/admin-notifications pushes real-time operator alerts to your team chat, gated by config.adminNotifications.enabled in packages/config/src/index.ts. When enabled (the shipped default), business events fan out to every configured chat provider via a throttled background worker; when off, notifyAdmins() is a no-op and the worker is not registered.

This is not the in-app bell. User-facing notifications are a separate channel - see Notifications. Admin notifications go to you (the operator), out-of-band, in chat.

What you get

  • A fire-and-forget notifyAdmins({ event, data }) helper - enqueues, never blocks the request.
  • A throttled, retrying worker that fans out to every configured provider at once.
  • Three chat providers: Discord, Slack, Telegram - auto-enabled by env.
  • Per-event opt-out via config.adminNotifications.events.

Configuration

config.adminNotifications is a discriminated union: { enabled: false } or { enabled: true; events? }.

KeyTypeDefaultDescription
enabledbooleantrueMaster flag; false makes notifyAdmins() a no-op and unregisters the worker.
eventsPartial<Record<Event, boolean>>?all onSet an event to false to suppress it.
packages/config/src/index.ts
adminNotifications: {
  enabled: true,
  events: { account_deleted: false } // suppress one event; the rest stay on
}

Credentials live in environment variables, not in config. A provider activates only when its env vars are set - enabled: true with no env vars sends nothing.

ProviderEnv vars
DiscordDISCORD_WEBHOOK_URL
SlackSLACK_WEBHOOK_URL
TelegramTELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, optional TELEGRAM_TOPIC_ID

See Environment Variables.

Events

These ten events trigger an alert (all on by default). They fire automatically from auth and payment webhooks - you do not wire them.

EventFires when
user_signupA new user registers
subscription_purchaseA subscription starts
lifetime_purchaseA lifetime plan is bought
plan_upgrade / plan_downgradeA plan changes tier
subscription_cancelA subscription is cancelled
refundA refund is processed
product_purchaseA one-time product is bought
credits_purchaseCredits are bought (custom or auto_topup)
account_deletedA user deletes their account

Sending an alert

notifyAdmins(notification) (packages/admin-notifications/src/service.ts) is the only call you make. It short-circuits via isAdminNotificationEnabled(event) - checking the master flag, at least one configured provider, and the per-event opt-out - then enqueues an admin-notification/send event. It never reaches a provider inline.

import { notifyAdmins } from "@repo/admin-notifications";

await notifyAdmins({
  event: "subscription_purchase",
  data: { entityType: "user", entityId, plan: "pro", interval: "month", amount: 20, currency: "USD" }
});

Most events carry an EntityRef (entityType + entityId); the worker resolves it to email/name at send time. refund and account_deleted carry email/name directly - by the time they run, the user row may be gone.

Delivery

The worker adminNotificationFunction (packages/api/src/functions/notifications/admin-notification.ts) is throttled to 10 events/second and retries up to 3 times. It resolves the entity, runs formatNotificationPayload() once into a provider-agnostic shape (title, message, color, fields), then sends to all providers with Promise.allSettled so one failing webhook never blocks the others.

It is only registered in packages/api/src/routes/inngest.ts when config.adminNotifications.enabled is true. See Background jobs.

Adding a provider

Each provider implements AdminNotificationProviderInterface from packages/admin-notifications/src/types.ts (send(payload) => Promise<{ success; messageId? }>).

Create a class in packages/admin-notifications/src/providers/ implementing the interface.
Register its credential env vars in packages/runtime/src/env.ts (providers read from env, not config).
Push an instance in getAdminNotificationProviders() (service.ts), guarded by the presence of those env vars.

Frequently asked questions

I enabled it but get no alerts - why? A provider activates only when its env vars are set. With enabled: true and no DISCORD_WEBHOOK_URL / SLACK_WEBHOOK_URL / Telegram vars, getAdminNotificationProviders() returns empty and the worker skips.

How do I silence one event? Set it false in config.adminNotifications.events (e.g. { user_signup: false }). Omitted events stay on.

Do alerts go to users? No. This channel is operator-only chat. For the user-facing bell and broadcasts, see Notifications.

Why didn't notifyAdmins() deliver instantly? It enqueues an admin-notification/send event; the throttled worker delivers it, retrying up to 3 times.

On this page