Sending Notifications
Overview
Sending happens in two steps. Notification() starts a builder you fill field by field; .build() closes it and gives you a SendableNotification, which carries the dispatch methods.
import {
GeneralSubject,
Notification,
} from "@antelopejs-private/cms/interfaces/cms-notifications";
await Notification()
.icon("i-ph-bell")
.title("Deployment finished")
.description("The staging deployment completed successfully.")
.subject(GeneralSubject)
.build()
.toUser(userId);
A SendableNotification is immutable and holds no recipient, so you can build once and dispatch several times.
| Method | Signature | Description |
|---|---|---|
toUser | (userId: string) => Promise<void> | Send to one user. |
toUsers | (userIds: string[], options?: SendOptions) => Promise<void> | Send to a list of users. |
toRoles | (roleIds: string[], options?: SendOptions) => Promise<void> | Send to every user holding at least one of the roles. |
broadcast | (options?: SendOptions) => Promise<void> | Send to every user. |
Building a Notification
Notification() takes no arguments and returns a fresh NotificationBuilder. Every setter stores its value and returns the builder, so calls chain in any order.
import { Notification } from "@antelopejs-private/cms/interfaces/cms-notifications";
import { orderSubject } from "./notifications";
const shipped = Notification()
.icon("i-ph-truck")
.title("$shop.notifications.order_shipped.title")
.description("$shop.notifications.order_shipped.description")
.params({ orderRef: "A-1043" })
.subject(orderSubject)
.linkTo("/shop/orders")
.build();
| Method | Type | Description |
|---|---|---|
.icon(value) | string | Icon shown next to the notification, in the dashboard icon syntax (for example i-ph-truck). Required. |
.title(value) | string | Title line. Required. |
.description(value) | string | Body line. Required. |
.subject(subject) | NotificationSubjectInfo | The subject the notification is filed under, which decides the category and the user preference it is gated on. Required. |
.linkTo(value) | string | Destination the notification links to, such as /settings/user/profile. Optional. |
.params(value) | Record<string, string | number> | Interpolation values carried alongside the notification. Optional. |
The subject comes from Categories and Subjects — either one you declared with NotificationSubject() or one of the built-in constants.
Titles and descriptions can be literal text or, following the DMS translation convention, a $-prefixed translation key; in the latter case the values passed to .params() are the interpolation values for that key. The CMS's own account notifications are written that way:
import {
Notification,
SecuritySubject,
} from "@antelopejs-private/cms/interfaces/cms-notifications";
await Notification()
.icon("i-ph-sign-in")
.title("$cms.notifications.messages.new_login.title")
.description("$cms.notifications.messages.new_login.description")
.params({ device: "Firefox on Linux", origin: " (203.0.113.4)" })
.subject(SecuritySubject)
.linkTo("/settings/user/profile")
.build()
.toUser(userId);
Required Fields Are Checked by the Type System
NotificationBuilder<Set> tracks in its type parameter which fields you have supplied: .icon() returns NotificationBuilder<Set | "icon">, .title() adds "title", and so on. build() is declared with a this parameter that only accepts a builder whose Set covers RequiredFields:
type RequiredFields = "icon" | "title" | "description" | "subject";
So a chain that skips one of the four does not compile — this is typed never at the build() call:
// Compile error: description and subject were never supplied
const broken = Notification().icon("i-ph-bell").title("Hello").build();
The check is purely static and happens at the call site. Nothing is validated again at runtime, and .linkTo() and .params() never affect it.
build() returns a SendableNotification holding a frozen copy of the data, so reusing the builder afterwards cannot change a notification you already built.
Sending to a Single User
toUser(userId) sends one copy to one user. It takes no options: a single recipient has no read scope to choose.
import { Controller, Post } from "@antelopejs/interface-api";
import { AuthRawUser } from "@antelopejs-private/cms/interfaces/cms-auth";
import type { User } from "@antelopejs-private/cms/interfaces/cms-auth/db";
import {
GeneralSubject,
Notification,
} from "@antelopejs-private/cms/interfaces/cms-notifications";
export class NotificationDemoController extends Controller("/api/notification") {
@Post("send")
async send(@AuthRawUser() user: User) {
await Notification()
.icon("i-ph-bell")
.title("Notification sent")
.description("This notification was sent from a route handler.")
.subject(GeneralSubject)
.build()
.toUser(user._id);
return { success: true };
}
}
Sending to Several Users
toUsers(userIds, options?) sends to a list of user ids. Duplicate ids in the list produce a single notification per user. The examples in this section and the two below dispatch the shipped notification built earlier on this page.
await shipped.toUsers([ownerId, managerId], { readScope: "shared" });
Sending to Roles
toRoles(roleIds, options?) resolves the users that hold any of the given roles and sends to them. An empty roleIds array sends nothing, and so does a set of roles nobody holds.
await shipped.toRoles([adminRoleId], { readScope: "shared" });
Role ids are the CMS role identifiers described in the DMS tenancy and permissions guide; membership is resolved across tenants, so a user holding the role in any tenant is included.
Broadcasting
broadcast(options?) sends to every user.
await shipped.broadcast();
Send Options
toUsers, toRoles and broadcast accept an optional SendOptions object.
| Option | Type | Description |
|---|---|---|
readScope | ReadScope (optional) | Read-state grouping for a multi-recipient send. Defaults to "individual" — see Read Scope. |
Pick "shared" for alerts that only need one person to act on them, and "individual" for anything each recipient has to see for themselves.
import type { SendOptions } from "@antelopejs-private/cms/interfaces/cms-notifications/types";
const forEveryone: SendOptions = { readScope: "individual" };
const handleOnce: SendOptions = { readScope: "shared" };
Delivery Rules
The CMS applies the same rules to every dispatch method, since toUsers, toRoles and broadcast all resolve to per-user deliveries:
- The subject must be registered. A notification whose subject was never declared through
NotificationSubject()is dropped. This is what makes registration order matter — see Declaration Order. - The user preference is honored. Delivery is skipped when the recipient has turned off the
categoryId:subjectIdpair in their notification settings. Preferences default to enabled, so a subject a user has never touched is delivered. - Skipping is silent. A gated-out delivery is not an error: the promise resolves normally, and there is no per-recipient result to inspect.
- Delivery is per recipient. Each recipient gets their own stored notification. With
readScope: "shared"those copies are tied together for read state; they are still separate rows. - Stored notifications are pushed live. Each delivery is also published on the recipient's realtime stream, so the dashboard's badge and preview update without a reload.
Because a send can be skipped for reasons outside your control, treat notifications as best-effort: do not make a workflow depend on one having been stored. Wrapping the call so a failure cannot break the operation that triggered it is a common pattern:
import { Logging } from "@antelopejs/interface-core/logging";
import {
AccountSubject,
Notification,
} from "@antelopejs-private/cms/interfaces/cms-notifications";
export async function notifyWelcome(userId: string, name: string) {
try {
await Notification()
.icon("i-ph-hand-waving")
.title("$cms.notifications.messages.welcome.title")
.description("$cms.notifications.messages.welcome.description")
.params({ name })
.subject(AccountSubject)
.linkTo("/settings/user/profile")
.build()
.toUser(userId);
} catch (error) {
Logging.Error(`Failed to notify "${userId}": ${String(error)}`);
}
}
Importing the Types
Notification() is exported from the interface root, and the data types from the types subpath. The two classes are exported from the modules that define them, which you only need when you want to name them explicitly:
| Symbol | Import from |
|---|---|
Notification | @antelopejs-private/cms/interfaces/cms-notifications |
NotificationData, SendOptions, RequiredFields | @antelopejs-private/cms/interfaces/cms-notifications/types |
NotificationBuilder | @antelopejs-private/cms/interfaces/cms-notifications/builder |
SendableNotification | @antelopejs-private/cms/interfaces/cms-notifications/sendable |
NotificationData is the shape the builder assembles — icon, title, description and subject, plus the optional linkTo and params. Naming it is useful when you carry notification content around before building it:
import { Notification } from "@antelopejs-private/cms/interfaces/cms-notifications";
import type { NotificationData } from "@antelopejs-private/cms/interfaces/cms-notifications/types";
import type { SendableNotification } from "@antelopejs-private/cms/interfaces/cms-notifications/sendable";
function fromData(data: NotificationData): SendableNotification {
const builder = Notification()
.icon(data.icon)
.title(data.title)
.description(data.description)
.subject(data.subject);
if (data.linkTo) {
builder.linkTo(data.linkTo);
}
if (data.params) {
builder.params(data.params);
}
return builder.build();
}