← All @molecule/* packages · App templates

@molecule/api-notification-center

Core interface · notification-center · API (Node) · v1.0.1 · Apache-2.0

Notification center core interface for molecule.dev

npm install @molecule/api-notification-center

npm · Source on GitHub

How it works

@molecule/api-notification-center is the notification-center core interface on the API (Node) side: the API your app calls, with no vendor inside.

Choose the implementation by bonding one of its 1 provider: @molecule/api-notification-center-database.

import { setProvider, send, getAll, markRead } from '@molecule/api-notification-center'
import { createProvider } from '@molecule/api-notification-center-database'

// Startup — after the @molecule/api-database DataStore is bonded
setProvider(createProvider())

// Send an in-app notification
const notification = await send('user-123', {
  type: 'system',
  title: 'Welcome!',
  body: 'Your account is ready.',
})

// List unread notifications
const { items, total } = await getAll('user-123', { read: false })

// Mark as read (scoped to the owner — only affects this user's row)
await markRead('user-123', notification.id)

Providers (1): @molecule/api-notification-center-database

Works with: @molecule/api-bond, @molecule/api-i18n

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Notification center core interface for molecule.dev.

Per-user, persistent IN-APP notifications (the bell-icon inbox): send, paginated listing, unread counts, mark-read, owner-scoped delete, and per-user notification preferences. For fire-and-forget ops/broadcast channels (Slack/webhook alerts), use @molecule/api-notifications instead.

Quick Start

import { setProvider, send, getAll, markRead } from '@molecule/api-notification-center'
import { createProvider } from '@molecule/api-notification-center-database'

// Startup — after the @molecule/api-database DataStore is bonded
setProvider(createProvider())

// Send an in-app notification
const notification = await send('user-123', {
  type: 'system',
  title: 'Welcome!',
  body: 'Your account is ready.',
})

// List unread notifications
const { items, total } = await getAll('user-123', { read: false })

// Mark as read (scoped to the owner — only affects this user's row)
await markRead('user-123', notification.id)

Type

core

Installation

npm install @molecule/api-notification-center @molecule/api-bond @molecule/api-i18n

API

Interfaces

BulkNotification

A bulk notification targeting a specific user.

interface BulkNotification {
  /** The target user. */
  userId: string

  /** The notification to send. */
  notification: CreateNotification
}

CreateNotification

Data required to create a new notification.

interface CreateNotification {
  /** Notification category. */
  type: string

  /** Notification headline. */
  title: string

  /** Notification body text. */
  body: string

  /** Arbitrary structured data to attach. */
  data?: Record<string, unknown>

  /** Delivery channels for this notification. Defaults to `['inApp']`. */
  channels?: ('inApp' | 'email' | 'push' | 'sms')[]
}

Notification

An in-app notification.

interface Notification {
  /** Provider-assigned notification identifier. */
  id: string

  /** The user this notification belongs to. */
  userId: string

  /** Notification category (e.g. 'system', 'message', 'alert'). */
  type: string

  /** Notification headline. */
  title: string

  /** Notification body text. */
  body: string

  /** Whether the notification has been read. */
  read: boolean

  /** Arbitrary structured data attached to the notification. */
  data?: Record<string, unknown>

  /** When the notification was created. */
  createdAt: Date
}

NotificationCenterProvider

Notification center provider interface.

All notification center providers must implement this interface to provide in-app notification CRUD, read status management, and user preferences.

interface NotificationCenterProvider {
  /**
   * Sends a notification to a specific user.
   *
   * @param userId - The target user identifier.
   * @param notification - The notification to create.
   * @returns The created notification.
   */
  send(userId: string, notification: CreateNotification): Promise<Notification>

  /**
   * Sends notifications to multiple users in a single batch.
   *
   * @param notifications - Array of user-targeted notifications.
   * @returns The created notifications.
   */
  sendBulk(notifications: BulkNotification[]): Promise<Notification[]>

  /**
   * Retrieves all notifications for a user with optional filtering.
   *
   * @param userId - The user to retrieve notifications for.
   * @param options - Optional query filters and pagination.
   * @returns Paginated notification results.
   */
  getAll(userId: string, options?: NotificationQuery): Promise<PaginatedResult<Notification>>

  /**
   * Returns the count of unread notifications for a user.
   *
   * @param userId - The user to count unread notifications for.
   * @returns The unread notification count.
   */
  getUnreadCount(userId: string): Promise<number>

  /**
   * Marks a single notification as read, scoped to its owner.
   *
   * Implementations MUST only affect rows where `user_id = userId` so a user
   * can never mark another user's notification read by id (IDOR).
   *
   * @param userId - The owner whose notification should be marked read.
   * @param notificationId - The notification to mark as read.
   * @returns `true` if a row owned by `userId` was updated, `false` otherwise.
   */
  markRead(userId: string, notificationId: string): Promise<boolean>

  /**
   * Marks all notifications for a user as read.
   *
   * @param userId - The user whose notifications should be marked read.
   */
  markAllRead(userId: string): Promise<void>

  /**
   * Deletes a notification, scoped to its owner.
   *
   * Implementations MUST only affect rows where `user_id = userId` so a user
   * can never delete another user's notification by id (IDOR).
   *
   * @param userId - The owner whose notification should be deleted.
   * @param notificationId - The notification to delete.
   * @returns `true` if a row owned by `userId` was deleted, `false` otherwise.
   */
  delete(userId: string, notificationId: string): Promise<boolean>

  /**
   * Retrieves notification preferences for a user.
   *
   * @param userId - The user to retrieve preferences for.
   * @returns The user's notification preferences.
   */
  getPreferences(userId: string): Promise<NotificationPreferences>

  /**
   * Updates notification preferences for a user.
   *
   * @param userId - The user to update preferences for.
   * @param preferences - The preferences to merge.
   */
  setPreferences(userId: string, preferences: Partial<NotificationPreferences>): Promise<void>
}

NotificationPreferences

User notification preferences.

interface NotificationPreferences {
  /** Whether email notifications are enabled. */
  email: boolean

  /** Whether push notifications are enabled. */
  push: boolean

  /** Whether SMS notifications are enabled. */
  sms: boolean

  /** Per-channel or per-type overrides. */
  channels: Record<string, boolean>
}

NotificationQuery

Query options for listing notifications.

interface NotificationQuery {
  /** Maximum number of results to return. */
  limit?: number

  /** Number of results to skip. */
  offset?: number

  /** Filter by read status. */
  read?: boolean

  /** Filter by notification type. */
  type?: string
}

PaginatedResult

Paginated result set.

interface PaginatedResult<T> {
  /** The result items for this page. */
  items: T[]

  /** Total number of matching items. */
  total: number

  /** Number of items skipped. */
  offset: number

  /** Maximum items per page. */
  limit: number
}

Functions

deleteNotification(userId, notificationId)

Deletes a notification, scoped to its owner.

Only deletes the notification when it belongs to userId, preventing a user from deleting another user's notification by id (IDOR).

function deleteNotification(userId: string, notificationId: string): Promise<boolean>
  • userId — The owner whose notification should be deleted.
  • notificationId — The notification to delete.

Returns: true if a row owned by userId was deleted, false otherwise.

getAll(userId, options)

Retrieves all notifications for a user with optional filtering.

function getAll(userId: string, options?: NotificationQuery): Promise<PaginatedResult<Notification>>
  • userId — The user to retrieve notifications for.
  • options — Optional query filters and pagination.

Returns: Paginated notification results.

getPreferences(userId)

Retrieves notification preferences for a user.

function getPreferences(userId: string): Promise<NotificationPreferences>
  • userId — The user to retrieve preferences for.

Returns: The user's notification preferences.

getProvider()

Retrieves the bonded notification center provider, throwing if none is configured.

function getProvider(): NotificationCenterProvider

Returns: The bonded notification center provider.

getUnreadCount(userId)

Returns the count of unread notifications for a user.

function getUnreadCount(userId: string): Promise<number>
  • userId — The user to count unread notifications for.

Returns: The unread notification count.

hasProvider()

Checks whether a notification center provider is currently bonded.

function hasProvider(): boolean

Returns: true if a notification center provider is bonded.

markAllRead(userId)

Marks all notifications for a user as read.

function markAllRead(userId: string): Promise<void>
  • userId — The user whose notifications should be marked read.

Returns: Resolves when all notifications have been marked read.

markRead(userId, notificationId)

Marks a single notification as read, scoped to its owner.

Only affects the notification when it belongs to userId, preventing a user from marking another user's notification read by id (IDOR).

function markRead(userId: string, notificationId: string): Promise<boolean>
  • userId — The owner whose notification should be marked read.
  • notificationId — The notification to mark as read.

Returns: true if a row owned by userId was updated, false otherwise.

send(userId, notification)

Sends a notification to a specific user.

function send(userId: string, notification: CreateNotification): Promise<Notification>
  • userId — The target user identifier.
  • notification — The notification to create.

Returns: The created notification.

sendBulk(notifications)

Sends notifications to multiple users in a single batch.

function sendBulk(notifications: BulkNotification[]): Promise<Notification[]>
  • notifications — Array of user-targeted notifications.

Returns: The created notifications.

setPreferences(userId, preferences)

Updates notification preferences for a user.

function setPreferences(
  userId: string,
  preferences: Partial<NotificationPreferences>,
): Promise<void>
  • userId — The user to update preferences for.
  • preferences — The preferences to merge.

Returns: Resolves when preferences have been updated.

setProvider(provider)

Registers a notification center provider as the active singleton. Called by bond packages during application startup.

function setProvider(provider: NotificationCenterProvider): void
  • provider — The notification center provider implementation to bond.

Available Providers

ProviderPackage
Notifications@molecule/api-notification-center-database

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-bond ^1.0.1
  • @molecule/api-i18n ^1.0.1

Runtime Dependencies

  • @molecule/api-bond

  • @molecule/api-i18n

  • Wire the database first — and migrate the tables. The database-backed bond (@molecule/api-notification-center-database) persists through the bonded @molecule/api-database DataStore: bond the DataStore before setProvider(createProvider()), and create the notifications and notification_preferences tables in your app's migrations (see the bond's docs for the expected columns) — nothing auto-creates them.

  • Every operation is owner-scoped by userIdmarkRead / deleteNotification affect only that user's row (returning false when nothing matched) and getAll returns only that user's items. In handlers, ALWAYS pass the AUTHENTICATED user's id — forwarding a client-supplied userId recreates the cross-user access the scoping exists to prevent.

  • getAll returns a paginated { items, total, offset, limit } result — drive the UI from items + total, not items.length.

  • Sending here does NOT deliver email/push — it writes the in-app record. Fan out to @molecule/api-emails / push packages separately (honoring getPreferences(userId)) when a notification should also leave the app.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual bell/feed and notification triggers, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • An event that should notify a user (a mention, invite, comment, a finished job — whatever this app defines) creates an in-app notification that appears in THAT user's bell/feed with the correct type, title, body, and any link/target carried in data. Sending here writes only the in-app record — do not expect it to also arrive by email/push.
  • The unread badge (from getUnreadCount) increments when a new notification arrives and equals the number of unread items shown in the feed.
  • Marking one read (markRead) flips that item to read and drops the badge by one; mark-all-read (markAllRead) shows every item as read and the badge as zero — and BOTH changes persist across a full reload (they are stored, not client-only state).
  • Real-time: if the app wires a live channel (SSE/websocket), a notification sent while the feed is open appears WITHOUT a manual reload and the badge updates live; with no live channel, confirm the new notification shows on the next feed load / poll.
  • Clicking a notification navigates to its target (the link/id in data) and, where that is the intended behavior, marks it read.
  • Clearing/deleting one (deleteNotification) removes it from the feed and it does NOT reappear on reload (it is deleted from the store, not just hidden client-side).
  • Scoping — a user sees ONLY their own feed: getAll returns just the authenticated user's items, a notification created for user A never shows for user B, and no route returns or mutates another user's notification by id (markRead/deleteNotification on someone else's id must no-op and return false, never touch that row). Handlers pass the AUTHENTICATED user's id — never a client-supplied userId.