BuildBaseBuildBase

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

ParameterTypeDefaultDescription
workspaceIdstringRequiredWorkspace the notification belongs to
eventstringRequiredEvent slug, e.g. comment_added
userIdstring-User to notify. Omit to notify every workspace member
dataNotificationData{}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.

FieldTypeDescription
titlestringPush title. Falls back to the event name
messagestringEmail body and push body
iconstringPush icon URL. Falls back to the org icon
imagestringLarge image in the push body
badgestringMonochrome status-bar icon (Android, ChromeOS)
urlstringClick destination. Also {{url}} in the email template
tagstringGrouping key - a new notification with the same tag replaces the previous one instead of stacking
actions{action, title, icon?}[]Action buttons, max 2
silentbooleanDeliver 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

ConditionResponse
Workspace does not exist404 - { success: false, message: 'Workspace "…" not found' }
userId does not exist404 - { success: false, message: 'User "…" not found' }
userId is not a member of the workspace404 - { success: false, message: 'User "…" is not a member of this workspace' }
More than 300 sends in an hour from one workspace429 - try again later
A new event slug, with the plan's custom events used up403 - { 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 403 above, worded for your end user. Slugs you already have keep sending.
  • The slug is stored lowercase. Letters, digits, ., _, : and - are kept, so comment_added stays comment_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:

  1. Org global switch - the developer turned off all email or all push.
  2. Event master switch - enabled: false on the event kills it outright.
  3. Event channel default - the developer disabled email or push for this specific event.
  4. 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:

ConditionResult
No event config exists in the databaseSends - backward compatibility for orgs predating the event registry. A send through the SDK registers it first
The gate throws unexpectedlySends - it never throws upward
Org settings row is missingFalls 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