← All @molecule/* packages · App templates
@molecule/api-notifications-preferencesAPI resource · notifications-preferences · API (Node) · v1.0.1 · Apache-2.0
Per-user channel toggles per notification type — single-row JSONB store with delivery-gate helpers.
npm install @molecule/api-notifications-preferences@molecule/api-notifications-preferences is an API resource: the routes, validation and storage for notifications-preferences, built on the database and auth cores so it runs on whichever providers your app has bonded.
import {
routes,
requestHandlerMap,
isEnabled,
} from '@molecule/api-notifications-preferences'
// Wire HTTP routes (mlcl inject does this automatically):
// GET /me/notification-preferences
// PUT /me/notification-preferences
// Gate delivery in a notification dispatcher:
if (await isEnabled(userId, 'order.shipped', 'email')) {
await sendEmail(...)
}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.tsJSDoc, not this file.
Notification preferences resource for molecule.dev.
Per-user channel toggles keyed by canonical event-type slug — used as the
delivery gate for email / push / sms / in-app notifications. Pairs with
@molecule/api-resource-notification (which stores the resulting
notifications) and the various dispatch bonds under
@molecule/api-notifications-*.
import {
routes,
requestHandlerMap,
isEnabled,
} from '@molecule/api-notifications-preferences'
// Wire HTTP routes (mlcl inject does this automatically):
// GET /me/notification-preferences
// PUT /me/notification-preferences
// Gate delivery in a notification dispatcher:
if (await isEnabled(userId, 'order.shipped', 'email')) {
await sendEmail(...)
}
resource
npm install @molecule/api-notifications-preferences @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource
NotificationChannelTogglesPer-channel enablement booleans for a single notification type.
interface NotificationChannelToggles {
/** Whether email delivery is enabled for this type. */
email: boolean
/** Whether push delivery is enabled for this type. */
push: boolean
/** Whether SMS delivery is enabled for this type. */
sms: boolean
/** Whether in-app inbox delivery is enabled for this type. */
inApp: boolean
}
NotificationPreferencesRowPersisted row shape in notifications_preferences.
Single row per user; preferences is a JSONB column holding the full
type→channel-toggles map.
interface NotificationPreferencesRow {
/** Owning user identifier. */
userId: string
/** The full preferences map. */
preferences: NotificationPreferences
/** When the row was created (ISO 8601). */
createdAt: string
/** When the row was last updated (ISO 8601). */
updatedAt: string
}
NotificationChannelDelivery channel name. Channels map 1:1 to dispatch bonds (email / push / sms / in-app inbox).
type NotificationChannel = 'email' | 'push' | 'sms' | 'inApp'
NotificationPreferencesFull preferences map: notification-type → per-channel toggles.
type NotificationPreferences = Record<NotificationType, NotificationChannelToggles>
NotificationPreferencesPatchPartial update payload — any subset of types, any subset of channels.
updatePreferences() deep-merges this into the stored map; missing keys
preserve their existing values rather than reverting to defaults.
type NotificationPreferencesPatch = Record<NotificationType, Partial<NotificationChannelToggles>>
NotificationTypeCanonical notification-type slug (e.g. order.shipped, streak.at_risk).
Aliased to string so applications can declare their own union of slugs
without coupling this package to any particular taxonomy.
type NotificationType = string
getPreferences(userId)Retrieves the stored preferences map for a user.
If no row exists yet, returns an empty map ({}). Callers should treat
missing entries as "all channels enabled" — use isEnabled() rather than
inspecting this map directly when gating delivery.
function getPreferences(userId: string): Promise<NotificationPreferences>
userId — The user to look up.Returns: The user's stored preferences map, or {} if no row exists.
getPreferencesHandler(req, res)Returns the current user's notification preferences map.
function getPreferencesHandler(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The authenticated request.res — The response object.isEnabled(userId, type, channel)Resolves whether a specific channel is enabled for a specific notification type for a user.
Default-on policy: returns true when no row exists, when the type has no
stored entry, or when the channel field is missing from the entry. Callers
should use this as the authoritative delivery gate.
function isEnabled(userId: string, type: string, channel: NotificationChannel): Promise<boolean>
userId — The user to check.type — The notification-type slug.channel — The delivery channel.Returns: true if delivery is allowed, false if explicitly disabled.
updatePreferences(userId, patch)Applies a partial update to the user's preferences map.
Performs a per-type, per-channel deep merge: keys present in patch
overwrite their counterparts in storage; keys absent from patch are
preserved. Creates the row on first call if none exists.
function updatePreferences(
userId: string,
patch: NotificationPreferencesPatch,
): Promise<NotificationPreferences>
userId — The user whose preferences should be updated.patch — Partial type→channel-toggle overrides to merge in.Returns: The fully-merged preferences map after the update.
updatePreferencesHandler(req, res)Merges a partial preferences patch into the current user's stored map.
function updatePreferencesHandler(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The authenticated request with a partial preferences body.res — The response object.requestHandlerMapHandler map for notification-preferences routes.
const requestHandlerMap: {
readonly getPreferences: typeof getPreferencesHandler
readonly updatePreferences: typeof updatePreferencesHandler
}
routesRoutes for reading and updating the current user's notification preferences.
const routes: readonly [
{
readonly method: 'get'
readonly path: '/me/notification-preferences'
readonly handler: 'getPreferences'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'put'
readonly path: '/me/notification-preferences'
readonly handler: 'updatePreferences'
readonly middlewares: readonly ['authenticate']
},
]
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-resource ^1.0.1@molecule/api-database
@molecule/api-i18n
@molecule/api-logger
@molecule/api-resource
Migration required. src/__setup__/notifications_preferences.sql ships
with this package and must exist in the target database before use
(scaffolded apps apply it automatically; when adding to an existing app,
apply it — adapted to your database bond — first).
Default-ON semantics: absence means ENABLED. A user with no row, no entry
for a type, or no field for a channel is opted IN. Always gate delivery with
isEnabled(userId, type, channel); never inspect the getPreferences() map
directly — naive missing-key checks either drop wanted notifications or spam
users who opted out.
The gate only works where it is called. Every server-side dispatch path
(email/push/sms/in-app senders, cron jobs, event hooks) must call
isEnabled() before sending. The shipped routes only manage stored
preferences; nothing enforces them automatically.
Routes are session-scoped (/me/notification-preferences, authenticate):
the handlers take the user from the session — never accept a target userId
from the client for reads or writes.
Type slugs are free-form strings: keep ONE canonical slug per event (e.g.
order.shipped) shared by the preferences UI and every dispatch call — a
mismatched slug silently bypasses the user's choice (default-on).
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A preference is only meaningful if it is HONORED at send time, so every saved toggle must also be proven to change what actually gets delivered. A box you can't check is an integration bug to fix — not a skip:
GET /me/notification-preferences returns an
empty map ({}), never a 500 or crash, and every channel
(email/push/sms/inApp) shows as ON — matching the default-on policy
(isEnabled() is true before any row exists).order.shipped) in the UI →
PUT /me/notification-preferences saves it → a reload / re-read shows that
channel false for that type while every other type and channel stays ON
(the partial merge does not clobber the rest).read_activity tool: the suppressed channel's message for that type is
ABSENT and the enabled channel's message is present. A dispatch path that
saves the toggle but never calls isEnabled() before sending is exactly
the bug this catches.isEnabled(). Confirm via read_activity that
the critical message is still captured./me/..., the owner taken from the session); no
request body, query, or path param lets a caller read or write another
user's preferences (no id-guessing into someone else's row).