npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/api-notifications-preferences

v1.0.1

Published

Per-user notification preferences (channel toggles per type) — single-row JSONB store with delivery-gate helpers.

Downloads

521

Readme

@molecule/api-notifications-preferences

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 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-*.

Quick Start

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(...)
}

Type

resource

Installation

npm install @molecule/api-notifications-preferences @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource

API

Interfaces

NotificationChannelToggles

Per-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
}

NotificationPreferencesRow

Persisted 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
}

Types

NotificationChannel

Delivery channel name. Channels map 1:1 to dispatch bonds (email / push / sms / in-app inbox).

type NotificationChannel = 'email' | 'push' | 'sms' | 'inApp'

NotificationPreferences

Full preferences map: notification-type → per-channel toggles.

type NotificationPreferences = Record<NotificationType, NotificationChannelToggles>

NotificationPreferencesPatch

Partial 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>>

NotificationType

Canonical 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

Functions

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.

Constants

requestHandlerMap

Handler map for notification-preferences routes.

const requestHandlerMap: {
  readonly getPreferences: typeof getPreferencesHandler
  readonly updatePreferences: typeof updatePreferencesHandler
}

routes

Routes 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']
  },
]

Injection Notes

Requirements

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

Runtime Dependencies

  • @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).

E2E Tests

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:

  • [ ] A brand-new user with no saved row opens the preferences screen and it renders with SANE DEFAULTS: 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).
  • [ ] Toggling a channel OFF for a category PERSISTS: disable one channel for one notification type (e.g. email for 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).
  • [ ] The opt-out is ACTUALLY HONORED end-to-end: after disabling email for a category, trigger that notification through its real flow — the email is SUPPRESSED on the disabled channel while an enabled channel (e.g. in-app/push) still delivers. Verify against the captured outbound with the 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.
  • [ ] Transactional/critical notifications that must always send (security alerts, receipts, password resets) STILL deliver on their channel even after the user has disabled that category/channel — their dispatch path is intentionally NOT gated by isEnabled(). Confirm via read_activity that the critical message is still captured.
  • [ ] AUTHORIZATION — a user reads and edits only THEIR OWN preferences. The routes are session-scoped (/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).