Writing Notifications
A notification has three parts: a type that names it, a via() function that picks the channels, and a build object with one function per channel that returns that channel's message.
import { defineNotification } from '@holo-js/notifications'
interface CommentRecipient {
readonly id: string
readonly type: string
readonly email: string
readonly name: string
readonly postTitle: string
readonly commentUrl: string
}
export default defineNotification({
type: 'comment-posted',
via(recipient: CommentRecipient) {
return ['email', 'database']
},
build: {
email(recipient) {
return {
subject: `New comment on "${recipient.postTitle}"`,
lines: ['Someone replied to your post.'],
action: { label: 'Read the comment', url: recipient.commentUrl },
}
},
database(recipient) {
return {
data: {
postTitle: recipient.postTitle,
url: recipient.commentUrl,
},
}
},
},
})The recipient
The first argument to notify() is the recipient (sometimes called the notifiable). Holo passes the same object to via() and to every builder in build.
Put everything the notification needs on that object: the fields Holo uses to reach the person (like email), plus the values your message uses (like postTitle). See how Holo finds where to deliver.
Type the recipient once, on the via() parameter. The builders pick up that type automatically, and notify() rejects recipients with the wrong shape:
via(recipient: CommentRecipient) {
return ['email', 'database']
},
build: {
email(recipient) {
return { subject: recipient.postTitle }
},
},The type
type is a name for the notification, such as 'comment-posted'. The database channel stores it with each row, so you can filter the inbox by it. Keep it stable once notifications are stored.
Choose channels
via() returns the channel names to deliver through. Every name it returns needs a matching builder in build, and TypeScript reports an error if one is missing.
Return different channels depending on the recipient:
via(recipient: CommentRecipient & { readonly wantsEmail: boolean }) {
return recipient.wantsEmail ? ['email', 'database'] : ['database']
},via() and each builder also receive a context object as their second argument. context.anonymous is true for on-demand notifications, and builders also get context.channel.
Build each channel's message
Each builder returns the message for one channel. The built-in channels expect these shapes:
build: {
email() {
return { subject: 'Your export is ready', lines: ['Download it from your dashboard.'] }
},
database() {
return { data: { exportId: 'exp_42' } }
},
broadcast() {
return { event: 'export.ready', data: { exportId: 'exp_42' } }
},
},See Channels for every field, and Custom channels to add your own, such as Slack or SMS.
WARNING
Builders run when you call notify(), even for queued notifications. Holo stores the built message, not the builder, so keep builders fast and free of side effects. Database and broadcast data must be JSON-serializable.
Queue and delay by default
A notification can declare that it should always be queued or delayed:
export default defineNotification({
type: 'weekly-summary',
via(recipient: { readonly id: string, readonly type: string, readonly email: string }) {
return ['email', 'database']
},
queue: { queue: 'notifications' },
delay: { email: 60 },
build: {
email() {
return { subject: 'Your weekly summary' }
},
database() {
return { data: { summary: 'weekly' } }
},
},
})See Queueing notifications for all forms of queue and delay.
Available options
Everything you can set on defineNotification(), plus the fields each built-in channel builder returns.
Definition options
Helpers
Email builder fields
Database builder fields
Broadcast builder fields
Builder context
defineNotification()
Checks a notification and returns it, ready to pass to notify(). It throws when via() or build is missing, when build has no channels, or when a builder isn't a function.
export default defineNotification({
type: 'comment-posted',
via: () => ['database'],
build: { database: () => ({ data: {} }) },
})isNotificationDefinition()
Returns true for a value that has via() and build. Use it to check that a value is a notification before you pass it along.
import { isNotificationDefinition } from '@holo-js/notifications'
if (isNotificationDefinition(value)) {
await notify(recipient, value)
}type
An optional name for the notification, such as 'comment-posted'. Database rows store it, so you can filter the inbox by it. Keep the name stable once notifications are stored.
defineNotification({ type: 'comment-posted', via: () => ['database'], build: { database: () => ({ data: {} }) } })via()
Required. Receives the recipient and returns the channel names to deliver through. Each name needs a matching builder in build.
via(recipient: CommentRecipient) {
return recipient.wantsEmail ? ['email', 'database'] : ['database']
}build
Required. An object with one builder per channel. Each builder receives (recipient, context) and returns that channel's message. Builders run when you call notify(), even for queued notifications.
build: {
database: () => ({ data: { exportId: 'exp_42' } }),
}queue
Queues the delivery of each channel. Pass true, an options object with connection, queue, delay, or afterCommit, or a function that returns one of those for each recipient and channel. Options passed to the send override these. See Queueing notifications.
queue: { queue: 'notifications', afterCommit: true }delay
Holds deliveries for a number of seconds, or until a Date. You can pass one value for every channel, an object with a value per channel such as { email: 600 }, or a function. Delayed deliveries are always queued. A delay passed to the send overrides this one.
delay: { email: 600 }email: subject
Required. The subject line, also shown as the heading of the generated email.
email() {
return { subject: 'Your export is ready' }
}email: greeting
An optional first paragraph, such as Hi Ava,.
email(recipient) {
return { subject: 'Welcome', greeting: `Hi ${recipient.name},` }
}email: lines
An optional list of paragraphs. Each line is trimmed, and blank lines are dropped.
email() {
return { subject: 'Export ready', lines: ['Your file is ready.', 'It expires in 7 days.'] }
}email: action
An optional button with a label and a url. Holo also prints the link as plain text under the button, for clients that hide buttons.
email(recipient) {
return {
subject: 'Reset your password',
action: { label: 'Reset password', url: recipient.resetUrl },
}
}email: html
An optional HTML body. When you set it, Holo sends this HTML instead of building one from greeting, lines, and action.
email() {
return { subject: 'Receipt', html: '<p>Thanks for your order.</p>' }
}email: text
An optional plain-text body. If you leave it out, Holo builds one from greeting, lines, and action.
email() {
return { subject: 'Receipt', text: 'Thanks for your order.' }
}email: metadata
An optional JSON object passed to the mail as its metadata.
email(recipient) {
return { subject: 'Receipt', metadata: { orderId: recipient.orderId } }
}database: data
Required. A JSON value stored with the notification and returned when you read the inbox.
database(recipient) {
return { data: { orderNumber: recipient.orderNumber } }
}broadcast: event
An optional event name that clients listen for. Defaults to notifications.message.
broadcast() {
return { event: 'orders.shipped', data: {} }
}broadcast: data
The payload sent to clients. Clients receive it with the notification's type.
broadcast(recipient) {
return { event: 'orders.shipped', data: { orderNumber: recipient.orderNumber } }
}context
The second argument to via() and to each builder. context.anonymous is true for on-demand notifications. Builders also receive context.channel, the name of the channel they build for.
email(recipient, context) {
return { subject: context.anonymous ? 'Sales alert' : `Hi ${recipient.name}` }
}Custom channels add their own builder names. See Custom channels.