Docs / Guides

Custom Services

notifly's plugin system lets you register any HTTP endpoint as a notification service. Once registered, your service works exactly like a built-in service — pass its URL to notify() and it dispatches alongside the others.

ServiceDefinition interface

typescript
interface ServiceDefinition {
  // The URL scheme(s) this service handles, e.g. ['myapp']
  schemas: string[];

  // Parse a URL object into a config object for send()
  parseUrl(url: URL): ServiceConfig;

  // Send the notification using the parsed config
  send(config: ServiceConfig, message: NotiflyMessage): Promise<NotiflyResult>;
}

Step-by-step example

Let's build a custom service for a fictional API at https://api.myapp.com/notify.

1. Define the URL scheme

Choose a URL scheme that doesn't conflict with built-ins. Your notification URLs will look like myapp://TOKEN/CHANNEL.

2. Implement parseUrl()

Extract credentials from the URL. The url argument is a standard URL object.

typescript
function parseUrl(url: URL) {
  // For myapp://TOKEN/CHANNEL
  // url.hostname → TOKEN
  // url.pathname → '/CHANNEL'
  return {
    service: 'myapp',
    token: url.hostname,
    channel: url.pathname.slice(1), // strip leading /
  };
}

3. Implement send()

typescript
async function send(config, message) {
  const res = await fetch(
    `https://api.myapp.com/channels/${config.channel}/messages`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${config.token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        title: message.title ?? '',
        text: message.body,
        level: message.type ?? 'info',
      }),
    }
  );

  if (!res.ok) {
    const body = await res.text();
    return {
      success: false,
      service: 'myapp',
      error: `HTTP ${res.status}: ${body}`,
    };
  }

  return { success: true, service: 'myapp' };
}

4. Register and use

complete example
import { registerService, notify } from '@ambersecurityinc/notifly';

registerService({
  schemas: ['myapp'],
  parseUrl(url) {
    return {
      service: 'myapp',
      token: url.hostname,
      channel: url.pathname.slice(1),
    };
  },
  async send(config, message) {
    const res = await fetch(
      `https://api.myapp.com/channels/${config.channel}/messages`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${config.token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ text: message.body }),
      }
    );
    return { success: res.ok, service: 'myapp' };
  },
});

// Use it like any built-in service
await notify(
  { urls: ['myapp://my-api-token/general'] },
  { title: 'Hello', body: 'Custom service works!' }
);

Multiple schemes

A single service definition can handle multiple schemes. This is useful for HTTP/HTTPS variants:

typescript
registerService({
  schemas: ['myapp', 'myapps'],  // myapp:// and myapps://
  parseUrl(url) {
    const secure = url.protocol === 'myapps:';
    return { service: 'myapp', secure, token: url.hostname };
  },
  async send(config, message) {
    const base = config.secure
      ? 'https://api.myapp.com'
      : 'http://api.myapp.com';
    // ...
  },
});

TypeScript types

typescript
import type {
  ServiceDefinition,
  ServiceConfig,
  NotiflyMessage,
  NotiflyResult,
} from '@ambersecurityinc/notifly';

Sharing custom services

Package your service definition as a standalone npm module and document its URL scheme format. Users install your package and call registerService() at startup.

← Previous Smart Paste Next → Cloudflare Workers