Overview
Send email and push notifications to a user or a whole workspace through one gated call.
bb.notification.send() delivers one notification across email and push at
once. Channel selection is not your decision at the call site — the org
settings, the event config, and the end user's workspace preferences decide
which channels actually fire.
import BuildBase from '@buildbase/sdk';
const bb = BuildBase({
serverUrl: 'https://api.console.buildbase.app',
orgId: process.env.BUILDBASE_ORG_ID!,
getSessionId: async () => getCookies().get('bb-session')?.value ?? null,
});
const result = await bb.notification.send(
workspaceId,
'comment_added',
userId,
{
message: 'Alice commented on your project',
url: '/projects/42#comment-8',
}
);
// `sent` only tells you a recipient was resolved — check `channels`
// to see whether email or push actually went out.
if (!result.channels.email && !result.channels.push) {
console.warn('Notification resolved a recipient but sent nothing.');
}Before you start
Create the notification event and its email template in the BuildBase console
under Notifications. An event with no template still fires push but
reports channels.email: false.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
workspaceId | string | Required | Workspace the notification belongs to |
event | string | Required | Event slug, e.g. comment_added |
userId | string | — | User to notify. Omit to notify every workspace member |
data | NotificationData | {} | Merge data for the template and the push payload |
Omitting userId is the workspace broadcast. It is an easy mistake to pass
null expecting a no-op and mail the entire workspace instead.
Notification data
message does double duty: it is the email body merge value and the push
notification body.
| Field | Type | Description |
|---|---|---|
title | string | Push title. Falls back to the event name |
message | string | Email body and push body |
icon | string | Push icon URL. Falls back to the org icon |
image | string | Large image in the push body |
badge | string | Monochrome status-bar icon (Android, ChromeOS) |
url | string | Click destination. Also {{url}} in the email template |
tag | string | Grouping key — a new notification with the same tag replaces the previous one instead of stacking |
actions | {action, title, icon?}[] | Action buttons, max 2 |
silent | boolean | Deliver without sound or vibration |
Three merge fields are injected for you and overwrite anything you pass under
the same key: name and email from the recipient, and workspaceName from
the workspace.
Restricting channels for one send
data.channels overrides the event's configured channels for this call only:
// Email only, even if the event also has push enabled
await bb.notification.send(workspaceId, 'invoice_ready', userId, {
message: 'Your invoice is ready',
channels: { email: true },
});The override is opt-in per channel. Once channels is present, a channel
missing from the object is treated as false — { email: true } disables push.
Omit channels entirely to use the event's configuration.
The override can only narrow, never widen: a channel the gates block stays blocked whether or not you request it.
What comes back
{
sent: true,
channels: { email: true, push: false },
notifiedCount: 1,
}channels reports what was actually used, not what was requested. A false
means that channel was blocked by the gates, was excluded by a data.channels
override, or — for email — that the event has no linked template.
Warning
sent does not mean delivered. It is notifiedCount > 0, and
notifiedCount counts the target users the call processed — it increments
once per user regardless of whether any channel fired. A call that resolves
one user and sends nothing returns sent: true with
channels: { email: false, push: false }.
Check channels to know whether anything actually went out.
NotificationResult declares an optional reason, but the current server
never populates it. Do not branch on it.
Errors
| Condition | Response |
|---|---|
| Workspace does not exist | 404 — { success: false, message: 'Workspace "…" not found' } |
userId does not exist | 404 — { success: false, message: 'User "…" not found' } |
A broadcast to a workspace with no members is not an error: it returns
sent: false with notifiedCount: 0.
The four gates
Every channel passes through canSendNotification() before it fires. The checks
run in order and the first failure wins:
- Org global switch — the developer turned off all email or all push.
- Event master switch —
enabled: falseon the event kills it outright. - Event channel default — the developer disabled email or push for this specific event.
- Workspace override — the end user's preference, honored only if the
event is marked
userManaged.
Layer 4 is the one that surprises people. An event that is not userManaged
ignores workspace preferences entirely, which is what you want for security
alerts and billing failures.
Fail-open behavior
The gate is deliberately permissive:
| Condition | Result |
|---|---|
| No event config exists in the database | Sends — backward compatibility for orgs predating the event registry |
| The gate throws unexpectedly | Sends — it never throws upward |
| Org settings row is missing | Falls through to the remaining layers |
Notifications fail open so a config gap cannot silently swallow a password reset. The trade-off is that a typo in an event slug produces a delivered notification rather than an error. Verify slugs against the 86+ events in the event catalog.
Next Steps
- Slack notifications — route the same events to a team channel.
- Push notifications — VAPID setup and subscriber management.
- Webhooks & events — the full event catalog.