GenerateSaaS

Admin Notifications

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

@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 through a throttled background worker.

This is not the in-app bell. Admin notifications go to you, out-of-band, in chat. For user-facing alerts see Notifications.

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; omitted events stay on.
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.

ProviderEnv vars
DiscordDISCORD_WEBHOOK_URL
SlackSLACK_WEBHOOK_URL
TelegramTELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, optional TELEGRAM_TOPIC_ID

A provider activates only when its env vars are set. enabled: true with no credentials sends nothing - getAdminNotificationProviders() returns empty and the worker skips. See Environment Variables.

Events

These fifteen events trigger an alert, all on by default. They fire automatically from auth, payment webhooks, the store lane, and the nightly webhook cleanup - the four store-lane events (duplicate_subscription to store_payment_attention) stay silent without a mobile app, and you wire none of 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
duplicate_subscriptionOne buyer pays for the same access twice, across lanes or stores
orphaned_member_purchaseAn org member buys their own plan while the owner already funds one
family_share_grantAccess arrives through an app store's Family Sharing
store_payment_attentionA store payment the automated paths could not settle
webhook_claim_abandonedThe nightly cleanup finds a webhook claimed over an hour ago that never finished - a payment that may not have been granted

Sending an alert

notifyAdmins(notification) (packages/admin-notifications/src/service.ts) is the only call you make. It short-circuits via isAdminNotificationEnabled(event), then enqueues an admin-notification/send event rather than reaching 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) that the worker resolves to email and name at send time. refund and account_deleted carry them 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 - one failing webhook never blocks the others.
  • It is registered in packages/api/src/routes/inngest.ts only 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.

On this page