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? }.
| 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 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.
| 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 |
duplicate_subscription | One buyer pays for the same access twice, across lanes or stores |
orphaned_member_purchase | An org member buys their own plan while the owner already funds one |
family_share_grant | Access arrives through an app store's Family Sharing |
store_payment_attention | A store payment the automated paths could not settle |
webhook_claim_abandoned | The 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 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.