Skip to content

Custom Channels ​

Custom channels use the same notification pipeline as built-in channels. Register the channel once, then return its name from via(...) and provide a matching payload builder under build.

Register a custom channel ​

ts
import {
  type NotificationChannel,
  registerNotificationChannel,
} from '@holo-js/notifications'

declare module '@holo-js/notifications' {
  interface HoloNotificationChannelRegistry {
    readonly slack: NotificationChannel<
      { readonly webhook: string },
      { readonly text: string },
      void
    >
  }
}

registerNotificationChannel('slack', {
  validateRoute(route) {
    if (!route.webhook.startsWith('https://')) {
      throw new Error('Slack webhooks must use https.')
    }

    return route
  },
  async send({ route, payload }) {
    await fetch(route.webhook, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        text: payload.text,
      }),
    })
  },
})

Use the channel in a notification ​

ts
import { defineNotification, notifyUsing } from '@holo-js/notifications'

const deploymentFinished = defineNotification({
  type: 'deployment-finished',
  via() {
    return ['slack']
  },
  build: {
    slack() {
      return {
        text: 'Deployment finished successfully.',
      }
    },
  },
})

await notifyUsing()
  .channel('slack', { webhook: 'https://hooks.slack.test/services/123' })
  .notify(deploymentFinished)

Type flow ​

Custom channel typing should flow through:

  • notifyUsing().channel(...)
  • defineNotification({ build: { ... } })
  • dispatch result channel names

That keeps route and payload validation local to the channel while preserving inference for application code.

Holo owns backend runtime concerns. The host framework owns SSR and routing.