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' } |
userId is not a member of the workspace | 404 - { success: false, message: 'User "…" is not a member of this workspace' } |
| More than 300 sends in an hour from one workspace | 429 - try again later |
| A new event slug, with the plan's custom events used up | 403 - { success: false, code: 'NOTIFICATION_EVENT_NOT_SET_UP', message: 'This notification type is not set up for this app. Ask its owner to add it.' } |
A broadcast to a workspace with no members is not an error: it returns
sent: false with notifiedCount: 0.
Events you have not set up
Note
Any SDK version; needs a BuildBase server from release 22 . On an older server the send still goes out, unregistered.
You do not have to create an event in the console before sending it. The first
send() with a slug the org has never seen registers it, and from then on it is
an ordinary custom event: listed in the console, where you can rename it, attach
an email template or switch it off, and mutable by each member from their
notification settings.
- It starts with push and the inbox only, which is what an unregistered send always delivered. Email starts once you attach a template.
- It counts toward your plan's custom notification events, the same as one
created in the console. With none left, a new slug is refused with the
403above, worded for your end user. Slugs you already have keep sending. - The slug is stored lowercase. Letters, digits,
.,_,:and-are kept, socomment_addedstayscomment_added; anything else becomes-.
Emails sent with send() count toward your plan's emails sent, like workflow
emails. Mail BuildBase sends for you, such as sign-in codes and invitations,
does not.
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.
- Preferences - honored only if the event is marked
userManaged. Each member's own choice in the workspace wins, then the workspace default. A default a workspace admin marked required is what every member gets.
Layer 4 is the one that surprises people. An event that is not userManaged
ignores preferences entirely, which is what you want for security alerts and
billing failures. Preferences decide how a member is interrupted, not whether
the notification is recorded: with both channels off it still appears in
their inbox.
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. A send through the SDK registers it first |
| 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 registers a new event and delivers it rather than erroring; it shows up in the console's event list. Verify slugs against the 129 events in the event catalog.
Every notification sent this way is also kept in each recipient's inbox, one item per person, whichever channels reached them.
Next Steps
- Notification inbox - the bell and inbox your users come back to.
- Slack notifications - route the same events to a team channel.
- Push notifications - VAPID setup and subscriber management.
- Webhooks & events - the full event catalog.