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
userIdstringUser 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' }

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:

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

ConditionResult
No event config exists in the databaseSends — backward compatibility for orgs predating the event registry
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 produces a delivered notification rather than an error. Verify slugs against the 86+ events in the event catalog.

Next Steps