Herald is a Go library that provides multi-channel notification delivery behind a single API surface. It routes notifications through Email (SMTP, Resend), SMS (Twilio), Push (FCM), and In-App (inbox) channels with pluggable provider drivers, a template engine with i18n support, user preference management, and scoped configuration overrides.
Herald is a library -- not a standalone service. You bring your own database, provider credentials, and process lifecycle. Herald provides the plumbing.
What it does01
Multi-channel delivery -- Send notifications across email, SMS, push, and in-app channels through a unified
SendandNotifyAPI. Each channel is backed by a pluggable driver that handles the transport-specific protocol.Template engine with i18n -- Define notification templates with Go template syntax, locale-specific versions, and typed variables with required/optional validation. The renderer resolves the best locale match (exact, language-only, or default fallback) and renders subject, HTML body, text body, and title fields.
In-app inbox -- The
inappchannel automatically stores notifications in a per-user inbox with read/unread tracking, unread counts, mark-all-read, and expiration support.User preferences -- Users can opt out of specific notification types on specific channels. The engine checks preferences before sending and silently skips opted-out notifications.
Scoped configuration -- Override provider selection, sender identity, and locale at the app, organization, or user level. The scope resolver walks the chain
user -> org -> app -> defaultto find the best match.Multiple store backends -- Four backends implement the same composite
store.Storeinterface:memory(testing),postgres(PostgreSQL via Grove),sqlite(SQLite via Grove), andmongo(MongoDB via Grove). All support Migrate, Ping, and Close lifecycle methods.Built-in drivers -- Five drivers ship out of the box:
smtp(standard SMTP with TLS),resend(Resend HTTP API),twilio(Twilio REST API),fcm(Firebase Cloud Messaging v1), andinapp(no-op driver for inbox storage).TypeID identifiers -- All entities use type-prefixed, K-sortable, UUIDv7-based identifiers. Each entity type has its own prefix (
hpvdfor providers,htplfor templates,hmsgfor messages, and so on), so passing the wrong ID type fails at parse time.Delivery log -- Every send attempt is recorded as a
message.Messagewith status tracking (queued,sending,sent,failed,bounced,delivered), provider attribution, and metadata.Forge extension -- Herald ships with a
extension.Extensionthat integrates with the Forge framework for automatic route registration, migration, health checks, DI container binding, and YAML-based configuration.
Design philosophy02
Library, not service. Herald is a set of Go packages you import. You control main, the database connection, the provider credentials, and the process lifecycle.
Interfaces over implementations. Every subsystem defines a Go interface (provider.Store, template.Store, message.Store, inbox.Store, preference.Store, scope.Store). Swap any storage backend with a single type change.
Composable stores. The store.Store interface composes all six subsystem store interfaces plus Migrate, Ping, and Close. Pass a single backend value wherever a store is needed.
Scope-aware delivery. The scope.Resolver walks the configuration hierarchy (user -> org -> app -> default) to resolve the correct provider and sender identity for each notification. This enables per-organization branding and per-user provider overrides without any application-level logic.
TypeID everywhere. All entities use type-prefixed, K-sortable identifiers via id.ID. Passing an hpvd_ (provider) ID where an htpl_ (template) ID is expected fails at parse time.
Quick look03
package main
import (
"context"
"fmt"
"log"
"github.com/xraph/herald"
"github.com/xraph/herald/driver/email"
"github.com/xraph/herald/driver/sms"
"github.com/xraph/herald/id"
"github.com/xraph/herald/provider"
"github.com/xraph/herald/store/memory"
"github.com/xraph/herald/template"
)
func main() {
ctx := context.Background()
// 1. Create an in-memory store.
store := memory.New()
// 2. Create the Herald engine with drivers.
h, err := herald.New(
herald.WithStore(store),
herald.WithDriver(&email.ResendDriver{}),
herald.WithDriver(&sms.TwilioDriver{}),
)
if err != nil {
log.Fatal(err)
}
// 3. Register an email provider with Resend credentials.
_ = store.CreateProvider(ctx, &provider.Provider{
ID: id.NewProviderID(),
AppID: "myapp",
Name: "Resend Production",
Channel: "email",
Driver: "resend",
Credentials: map[string]string{
"api_key": "re_123abc",
},
Settings: map[string]string{
"from": "[email protected]",
"from_name": "My App",
},
Priority: 1,
Enabled: true,
})
// 4. Create a template with an English version.
tmpl := &template.Template{
ID: id.NewTemplateID(),
AppID: "myapp",
Slug: "order.confirmation",
Name: "Order Confirmation",
Channel: "email",
Category: "transactional",
Variables: []template.Variable{
{Name: "user_name", Type: "string", Required: true},
{Name: "order_id", Type: "string", Required: true},
},
Enabled: true,
}
_ = store.CreateTemplate(ctx, tmpl)
_ = store.CreateVersion(ctx, &template.Version{
ID: id.NewTemplateVersionID(),
TemplateID: tmpl.ID,
Locale: "en",
Subject: "Order {{.order_id}} confirmed",
Text: "Hi {{.user_name}}, your order {{.order_id}} has been confirmed.",
Active: true,
})
// 5. Send the notification.
result, err := h.Send(ctx, &herald.SendRequest{
AppID: "myapp",
Channel: "email",
Template: "order.confirmation",
To: []string{"[email protected]"},
Data: map[string]any{
"user_name": "Alice",
"order_id": "ORD-42",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("sent: message_id=%s status=%s\n", result.MessageID, result.Status)
// Output: sent: message_id=hmsg_01h... status=sent
}