@krizaka/notifications
The bell, channels and preferences: one notify writes the item and one outbox message per enabled channel; burst caps hold under concurrency; the admin tunes defaults.
Install
npm install @krizaka/notificationsAlso published as subpath exports: @krizaka/notifications/settings, @krizaka/notifications/testkit.
The bell, the channels and the preferences for every Krizaka product. One notify writes the in-app item and appends one outbox message per enabled channel, respecting the account's matrix and a burst cap per category. No push or realtime import (the workers that deliver a channel are the product's), no database client, no wall clock.
- Channels.
app(the bell and the live event of the open pages, one switch),push(one message per registered device) andemail. The outbox topics arenotifications.live,notifications.pushandnotifications.email; the event isevt.notification.requested.v1, the name and the shape of Java'skrizaka-notifications-apirequest (channel,recipient,template,locale,variables, plus the ids;channelisEMAIL, andPUSH/LIVEfor the web). A product delivers e-mail with its own worker (@krizaka/emailto render,@krizaka/mailto queue) or lets the Java service consume it. - The product declares its categories and their defaults (
notificationSettings(declared)is the registry fragment); the admin tunes the default channels and the caps without a redeploy. Alockedcategory (security, the account) cannot be switched off by anyone. - Burst caps. After
burstMaxnotifications of one category to one account withinburstWindowSeconds, the next ones are bell-only: the item is kept, no channel is used. The count and the insert run under a transaction-level advisory lock per account and category, so the cap holds under concurrency whennotifygets the caller'stx. - Idempotent.
dedupeKeymakes one fact one notification; the outbox message ids derive from the item, so a retried append is a no-op. - The bell.
list(newest first, cursor),unreadCount,markRead(the account's own items; the firstread_atstays). An item whoseappchannel was off is kept for the history and the cap, never shown. - Preferences.
preferences(every category by channel with its source: default, account, locked),setPreference,setMatrix(a preset applied). - Devices.
registerDevice(a token belongs to one account; registering it again moves it),revokeDevicewhen the push service refuses it. - Privacy. The module holds no address (the product passes
emailper call);erase(accountId)removes the items, choices and devices;purgeapplies the retention.
import { createOutbox } from "@krizaka/outbox";
import { createNotifications, dbNotificationStore } from "@krizaka/notifications";
import { notificationSettings, readNotificationConfig } from "@krizaka/notifications/settings";
const declared = {
messages: { defaults: { app: true, push: true, email: true }, burst: { max: 5, windowSeconds: 3600 } },
follows: { defaults: { app: true, push: false, email: false } },
account: { locked: true, defaults: { app: true, push: true, email: true } },
};
// settingsRegistry = { ...otherSettings, ...notificationSettings(declared) }
const notifications = createNotifications({
clock,
outbox: createOutbox({ clock, settings: outboxSettings }),
config: () => readNotificationConfig(declared, (key) => settings.get(key)),
});
// in the use case's transaction
await notifications.notify(tx, { accountId, category: "messages", template: "new-message", variables: { name }, locale: "fr", email, dedupeKey: `message:${messageId}` });Tables (schema notifications, notificationsSchema): item, preference, device. notificationStoreContract (@krizaka/notifications/testkit) is the contract every store runs.
Apache-2.0.
Exports
The 38 names the package exports, read from its types.
| Name | Kind | What it is |
|---|---|---|
CategoryDefaults | type | A category as the product declares it. |
categoryOf | function | |
Channel | type | |
CHANNEL | constant | The channels an account switches. |
CHANNELS | constant | |
checkConfig | function | |
checkInput | function | |
Clock | type | |
createNotifications | function | The notifications of a product. |
dbNotificationStore | function | The PostgreSQL store of « notifications » over a Query, in schema notifications (or the one the product renamed it to). |
Delivery | type | |
DELIVERY | constant | What the outbox carries: the channel of Java's evt.notification.requested.v1 (EMAIL) and two more for the web (PUSH, LIVE). |
Device | type | |
Item | type | One line of the bell. |
memoryNotificationStore | function | An in-memory store, for unit tests and demos: the behaviour of the database's (the contract kit runs on both). |
NOTIFICATION_BOUNDS | constant | |
NOTIFICATION_CODE | constant | |
NOTIFICATION_DEFAULTS | constant | |
NOTIFICATION_EVENT | constant | The event the outbox carries, the name Java's krizaka-notifications-api gives its request. |
NOTIFICATION_TOPIC | constant | One topic per delivery, so a worker claims only what it delivers (notifications.email, notifications.push, notifications.live). |
NotificationCode | type | |
NotificationConfig | type | The categories (declared by the product, tuned by the admin) and the retention. |
NotificationDeps | type | |
NotificationError | class | |
NotificationOutbox | type | What the module needs of @krizaka/outbox (its Outbox satisfies it): append in the caller's transaction. |
NotificationRequest | type | The request one delivery worker receives: Java's NotificationRequest (channel, recipient, template, locale, variables) plus the ids. |
Notifications | type | |
notificationsSchema | constant | The tables of « notifications », in their own Postgres schema: item (the bell, and the history the burst caps count), preference (the account's explicit… |
NotificationStore | type | Where the state lives. |
Notified | type | |
NotifyInput | type | |
Platform | type | |
PLATFORM | constant | |
PreferenceView | type | What an account sees in its settings: every category, every channel, and why. |
Query | type | |
resolveChannels | function | The channels on for an account: the category's defaults, overridden by the account's choices, unless the category is locked. |
uuidOf | function | A stable UUID (version 5 shaped) from a seed: the same fact appended twice is one outbox message. |
viewOf | function |
@krizaka/notifications/settings
| Name | Kind | What it is |
|---|---|---|
CategoryDeclaration | type | What a product declares for a category; the admin tunes the values (except locked, which is code). |
Declared | type | |
notificationSettings | function | The settings of « notifications » for the categories a product declares, a fragment it adds to its registry (@krizaka/notifications/settings): for each cat… |
readNotificationConfig | function | The module's configuration as createNotifications takes it, from the product's accessor ((key) => service.get(key, ctx)). |
@krizaka/notifications/testkit
| Name | Kind | What it is |
|---|---|---|
at | function | |
itemOf | function | An item for tests; the account ids are UUIDs because the database's are. |
notificationStoreContract | function | What every NotificationStore adapter must do: the same behaviour behind the same port. |
T0 | constant |
Package
| Version on npm | 0.2.2 (npm) |
| Licence | Apache-2.0 |
| Node.js | >=22 |
| Dependencies | @krizaka/db workspace:^, @krizaka/settings workspace:^, @krizaka/time workspace:^ |
| Peer dependencies | vitest >=4, zod >=3.23 |
| Source | notifications |
Changelog
0.2.2
Patch changes
- The READMEs, comments and published declarations no longer name the products that left the organisation (krizaka/.github#413, rule
no-departed-published). The sources were cleaned in #262 with an empty changeset, so the published tarballs still carried the old text; this release replaces them. No behaviour change. - Updated dependencies [2a7898c]
- Updated dependencies [2272b57]
- @krizaka/[email protected]
- @krizaka/[email protected]
0.2.1
Patch changes
- Updated dependencies [0d19b64]
- @krizaka/[email protected]
- @krizaka/[email protected]
0.2.0
Minor changes
- New package (#60):
@krizaka/notifications.createNotifications(notifywriting the bell item and one outbox message per enabled channel: live, e-mail, push per device, asevt.notification.requested.v1in the shape of Java's request;list,unreadCount,markRead,preferences,setPreference,setMatrix,registerDevice,revokeDevice,purge,erase), per-category burst caps that hold under concurrency (advisory lock per account and category), locked categories,dedupeKey,notificationsSchema(notifications.item,notifications.preference,notifications.device),dbNotificationStore,memoryNotificationStore,@krizaka/notifications/settings(notificationSettings(declared),readNotificationConfig) andnotificationStoreContract.
Patch changes
- Updated dependencies [8210a9a]
- @krizaka/[email protected]
@krizaka/storage
Files behind one port: content-hashed names, signed and public links; an HTTP zone, any S3-compatible bucket or a folder, chosen by the admin; bytes checked by magic number.
@krizaka/mail
Queued mail sent after the commit, once per key, never to a suppressed address; the provider is the admin's choice; bounces and complaints suppress.