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; when off, notifyAdmins() is a no-op and the worker is not registered.
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? }.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master flag; false makes notifyAdmins() a no-op and unregisters the worker. |
events | Partial<Record<Event, boolean>>? | all on | Set an event to false to suppress it; omitted events stay on. |
adminNotifications: {
enabled: true,
events: { account_deleted: false } // suppress one event; the rest stay on
}Credentials live in environment variables, not in config.
| Provider | Env vars |
|---|---|
| Discord | DISCORD_WEBHOOK_URL |
| Slack | SLACK_WEBHOOK_URL |
| Telegram | TELEGRAM_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 ten events trigger an alert, all on by default. They fire automatically from auth and payment webhooks - you do not wire them.
| Event | Fires when |
|---|---|
user_signup | A new user registers |
subscription_purchase | A subscription starts |
lifetime_purchase | A lifetime plan is bought |
plan_upgrade / plan_downgrade | A plan changes tier |
subscription_cancel | A subscription is cancelled |
refund | A refund is processed |
product_purchase | A one-time product is bought |
credits_purchase | Credits are bought (custom or auto_topup) |
account_deleted | A 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) - master flag, at least one configured provider, 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) that the worker resolves to email and name at send time. refund and account_deleted carry email and 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 withPromise.allSettled- one failing webhook never blocks the others. - It is registered in
packages/api/src/routes/inngest.tsonly whenconfig.adminNotifications.enabledistrue. See Background jobs.
Adding a provider
Each provider implements AdminNotificationProviderInterface from packages/admin-notifications/src/types.ts (send(payload) => Promise<{ success; messageId? }>).
packages/admin-notifications/src/providers/ implementing the interface.packages/runtime/src/env.ts - providers read from env, not config.getAdminNotificationProviders() (service.ts), guarded by the presence of those env vars.