Skip to content

jeromesth/Herald

Repository files navigation

Herald

Open-source headless notification system for TypeScript.
Build notification workflows on your own infrastructure.

Quickstart · Features · Adapters · Roadmap


Herald is a headless, open-source notification infrastructure library for TypeScript. It provides a complete notification system — subscribers, workflows, preferences, channels, and an in-app inbox — without locking you into a SaaS platform.

Think of it as the better-auth for notifications: one config file, bring your own database, bring your own workflow engine.

Why Herald?

SaaS (Novu, Knock) Herald
Hosting Vendor-managed Your infrastructure
Data Stored externally Your database
Pricing Per-notification Free forever
Workflow engine Proprietary Bring your own (Inngest, Temporal, etc.)
Database Proprietary Bring your own (Prisma, Drizzle, etc.)
Customization Limited API Full source access + plugin system

Quickstart

Install

# Core library
pnpm add @jeromesth/herald

# Pick your database adapter
pnpm add @prisma/client

# Pick your workflow engine
pnpm add inngest

Configure

Herald follows the same single-config pattern as better-auth. One file, one source of truth:

// lib/notifications.ts
import { herald } from "@jeromesth/herald";
import { prismaAdapter } from "@jeromesth/herald/prisma";
import { inngestAdapter } from "@jeromesth/herald/inngest";
import { PrismaClient } from "@prisma/client";
import { Inngest } from "inngest";

const prisma = new PrismaClient();
const inngest = new Inngest({ id: "my-app" });

export const notifications = herald({
  appName: "My App",
  basePath: "/api/notifications",

  // Bring your own database
  database: prismaAdapter(prisma, { provider: "postgresql" }),

  // Bring your own workflow engine
  workflow: inngestAdapter({ client: inngest }),

  // Define your notification workflows
  workflows: [
    {
      id: "welcome",
      name: "Welcome Notification",
      steps: [
        {
          stepId: "in-app",
          type: "in_app",
          handler: async ({ subscriber, payload }) => ({
            subject: "Welcome!",
            body: `Hello ${subscriber.externalId}, welcome to our platform!`,
            actionUrl: "/getting-started",
          }),
        },
        {
          stepId: "send-email",
          type: "email",
          handler: async ({ subscriber, payload }) => ({
            subject: "Welcome aboard!",
            body: `We're glad to have you, ${subscriber.firstName}!`,
          }),
        },
      ],
    },
  ],

  // Default preferences
  defaultPreferences: {
    channels: { in_app: true, email: true },
  },

  // Extend with plugins
  plugins: [],
});

Mount the API

Herald generates REST endpoints automatically. Mount them in your framework:

// Next.js App Router
// app/api/notifications/[...path]/route.ts
import { notifications } from "@/lib/notifications";

export const GET = notifications.handler;
export const POST = notifications.handler;
export const PUT = notifications.handler;
export const PATCH = notifications.handler;
export const DELETE = notifications.handler;
// Express / Hono / any framework
app.all("/api/notifications/*", (req) => notifications.handler(req));

Note: herald().handler has no built-in authentication. Add your auth middleware before mounting it in production.

Trigger Notifications

// From your API routes or server actions
import { notifications } from "@/lib/notifications";

// Trigger a workflow
await notifications.api.trigger({
  workflowId: "welcome",
  to: "user-123",
  payload: { planName: "Pro" },
});

// Trigger for multiple recipients
await notifications.api.trigger({
  workflowId: "team-invite",
  to: ["user-1", "user-2", "user-3"],
  payload: { teamName: "Engineering" },
});

Manage Subscribers

// Create or update a subscriber
await notifications.api.upsertSubscriber({
  externalId: "user-123",
  email: "alice@example.com",
  firstName: "Alice",
  data: { plan: "pro" },
});

// Get notifications (in-app inbox)
const { notifications: items, totalCount } = await notifications.api.getNotifications({
  subscriberId: "user-123",
  read: false,
  limit: 20,
});

// Mark as read
await notifications.api.markNotifications({
  ids: ["notif-1", "notif-2"],
  action: "read",
});

// Update preferences
await notifications.api.updatePreferences("user-123", {
  channels: { email: false },
  workflows: { "marketing-digest": false },
});

Features

Core

  • Single configuration file — one herald() call configures everything
  • Type-safe — full TypeScript types and inference throughout
  • Framework agnostic — works with Next.js, Express, Hono, Fastify, or any framework
  • Headless — no UI opinions, bring your own frontend

Notification System

  • Multi-channel delivery — in-app and email (SMS, push coming soon)
  • Workflow engine — define notification flows with steps, delays, and digests
  • Subscriber management — create, update, and manage notification recipients
  • In-app inbox — query notifications with read/seen/archived state
  • Notification preferences — per-channel, per-workflow, per-category opt-in/opt-out
  • Topics — group subscribers for fan-out notifications
  • Plugin system — extend Herald with custom logic, schemas, and endpoints

REST API

Herald auto-generates these REST endpoints:

Method Path Description
POST /trigger Trigger a notification workflow
POST /trigger/bulk Trigger multiple workflows at once
DELETE /trigger/:transactionId Cancel an in-flight workflow
POST /subscribers Create or update a subscriber
GET /subscribers/:id Get a subscriber
PATCH /subscribers/:id Update a subscriber
DELETE /subscribers/:id Delete a subscriber
GET /notifications/:subscriberId List notifications (inbox)
GET /notifications/:subscriberId/count Get notification count
POST /notifications/mark Mark notifications read/seen/archived
POST /notifications/mark-all-read Mark all as read
GET /subscribers/:id/preferences Get subscriber preferences
PUT /subscribers/:id/preferences Update preferences
POST /topics Create a topic
GET /topics List topics
GET /topics/:key Get a topic
DELETE /topics/:key Delete a topic
POST /topics/:key/subscribers Add subscribers to topic
DELETE /topics/:key/subscribers Remove subscribers from topic

Adapters

Database Adapters

Herald uses a generic database adapter interface (same pattern as better-auth). Bring your own ORM:

Adapter Import Status
Prisma @jeromesth/herald/prisma Available
Drizzle @jeromesth/herald/drizzle Available
Kysely @jeromesth/herald/kysely Planned
MikroORM @jeromesth/herald/mikro-orm Planned
MongoDB @jeromesth/herald/mongo Planned

Workflow Adapters

Herald delegates workflow execution to your preferred engine:

Adapter Import Status
Inngest @jeromesth/herald/inngest Available
Postgres @jeromesth/herald/postgres Available
Upstash Workflow @jeromesth/herald/upstash Available
Temporal @jeromesth/herald/temporal Planned
Trigger.dev @jeromesth/herald/trigger Planned

Database Schema

Herald creates these tables in your database:

Table Purpose
subscriber Notification recipients with contact info and metadata
notification Delivered notifications with delivery/engagement status
topic Named groups for fan-out notifications
topicSubscriber Many-to-many relationship between topics and subscribers
preference Per-subscriber notification preferences
channel Configured delivery channels (email providers, etc.)

Plugins

Herald supports a plugin system inspired by better-auth. Plugins can:

  • Extend the database schema — add new tables or fields to existing tables
  • Add REST endpoints — register new API routes
  • Hook into lifecycle events — intercept triggers, sends, and more
  • Inject context — add custom data to the Herald context
import { herald } from "@jeromesth/herald";
import type { HeraldPlugin } from "@jeromesth/herald";

const analyticsPlugin: HeraldPlugin = {
  id: "analytics",
  schema: {
    notificationEvent: {
      fields: {
        id: { type: "string", required: true, unique: true },
        notificationId: { type: "string", required: true },
        event: { type: "string", required: true },
        timestamp: { type: "date", required: true },
      },
    },
  },
  hooks: {
    afterSend: async ({ subscriberId, channel, messageId }) => {
      console.log(`Notification ${messageId} sent to ${subscriberId} via ${channel}`);
    },
  },
};

const notifications = herald({
  // ...config
  plugins: [analyticsPlugin],
});

Tech Stack

  • TypeScript — full type safety
  • pnpm — package management
  • Node.js — runtime (>=20.0.0)
  • Vitest — testing
  • Biome — linting and formatting

Contributing

We welcome contributions! See ROADMAP.md for planned features and areas where help is needed.

# Clone and setup
git clone https://github.com/jeromesth/herald.git
cd herald
pnpm install

# Run tests
pnpm test

# Lint
pnpm lint

License

MIT License - see LICENSE for details.

About

Open source headless alternatives to notifications system like knock.app. Inspired by BetterAuth

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors