Entities
Core data types used across Herald's notification subsystems.
Herald defines a set of entity types across its subsystem packages. Each entity uses an id.ID identifier with an entity-specific prefix and carries an AppID field for multi-tenant isolation.
Provider
Package: github.com/xraph/herald/provider
ID prefix: hpvd_
A Provider represents a configured notification delivery backend. Each provider is scoped to an app, handles a single channel type, and is bound to a specific driver.
type Provider struct {
ID id.ProviderID `json:"id"`
AppID string `json:"app_id"`
Name string `json:"name"`
Channel string `json:"channel"`
Driver string `json:"driver"`
Credentials map[string]string `json:"credentials,omitempty"`
Settings map[string]string `json:"settings,omitempty"`
Priority int `json:"priority"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}| Field | Description |
|---|---|
Channel | The notification channel: "email", "sms", "push", or "inapp". |
Driver | The driver implementation name (e.g. "smtp", "resend", "twilio", "fcm", "inapp"). |
Credentials | Driver-specific credentials (API keys, SMTP passwords). Stored encrypted at rest. |
Settings | Driver-specific settings (e.g. "from", "from_name", "region"). |
Priority | Lower numbers indicate higher priority. When the scope resolver falls back to listing all providers for a channel, it picks the lowest priority enabled provider first. |
Enabled | When false, the provider is skipped during resolution. |
Provider store interface
type Store interface {
CreateProvider(ctx context.Context, p *Provider) error
GetProvider(ctx context.Context, providerID id.ProviderID) (*Provider, error)
UpdateProvider(ctx context.Context, p *Provider) error
DeleteProvider(ctx context.Context, providerID id.ProviderID) error
ListProviders(ctx context.Context, appID string, channel string) ([]*Provider, error)
ListAllProviders(ctx context.Context, appID string) ([]*Provider, error)
}Supported channels and drivers
| Channel | ChannelType constant | Built-in drivers |
|---|---|---|
herald.ChannelEmail ("email") | smtp, resend | |
| SMS | herald.ChannelSMS ("sms") | twilio |
| Push | herald.ChannelPush ("push") | fcm |
| In-App | herald.ChannelInApp ("inapp") | inapp |
Template
Package: github.com/xraph/herald/template
ID prefix: htpl_
A Template defines the content and structure of a notification. Templates are identified by a unique slug + channel combination within an app and support multiple locale-specific versions for internationalization.
type Template struct {
ID id.TemplateID `json:"id"`
AppID string `json:"app_id"`
Slug string `json:"slug"`
Name string `json:"name"`
Channel string `json:"channel"`
Category string `json:"category"`
Variables []Variable `json:"variables,omitempty"`
Versions []Version `json:"versions,omitempty"`
IsSystem bool `json:"is_system"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}| Field | Description |
|---|---|
Slug | URL-safe identifier (e.g. "auth.welcome", "order.shipped"). Unique per app + channel. |
Category | Organizational category: "auth", "transactional", "marketing", or "system". |
Variables | Declared template variables with types, defaults, and required flags. |
IsSystem | System templates are seeded automatically and cannot be deleted via the API. |
Enabled | When false, sending with this template returns ErrTemplateDisabled. |
Category constants
const (
CategoryAuth = "auth"
CategoryTransactional = "transactional"
CategoryMarketing = "marketing"
CategorySystem = "system"
)Variable
A Variable describes an expected placeholder in the template content:
type Variable struct {
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Default string `json:"default,omitempty"`
Description string `json:"description,omitempty"`
}When Required is true and the caller does not provide the variable in the Data map, rendering returns ErrMissingRequiredVariable.
Template Version
ID prefix: htpv_
A Version represents a locale-specific content variant of a template. Each version contains the actual rendered content (subject, HTML, text, title) for a single locale.
type Version struct {
ID id.TemplateVersionID `json:"id"`
TemplateID id.TemplateID `json:"template_id"`
Locale string `json:"locale"`
Subject string `json:"subject,omitempty"`
HTML string `json:"html,omitempty"`
Text string `json:"text,omitempty"`
Title string `json:"title,omitempty"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}| Field | Description |
|---|---|
Locale | BCP 47 locale tag (e.g. "en", "fr", "pt-BR"). Unique per template. |
Subject | Email subject line (email channel). |
HTML | HTML body (email channel). |
Text | Plain-text body (email, SMS channels). |
Title | Notification title (push, in-app channels). |
Active | When false, this version is skipped during locale resolution. |
Template store interface
type Store interface {
// Template CRUD
CreateTemplate(ctx context.Context, t *Template) error
GetTemplate(ctx context.Context, templateID id.TemplateID) (*Template, error)
GetTemplateBySlug(ctx context.Context, appID string, slug string, channel string) (*Template, error)
UpdateTemplate(ctx context.Context, t *Template) error
DeleteTemplate(ctx context.Context, templateID id.TemplateID) error
ListTemplates(ctx context.Context, appID string) ([]*Template, error)
ListTemplatesByChannel(ctx context.Context, appID string, channel string) ([]*Template, error)
// Version CRUD
CreateVersion(ctx context.Context, v *Version) error
GetVersion(ctx context.Context, versionID id.TemplateVersionID) (*Version, error)
UpdateVersion(ctx context.Context, v *Version) error
DeleteVersion(ctx context.Context, versionID id.TemplateVersionID) error
ListVersions(ctx context.Context, templateID id.TemplateID) ([]*Version, error)
}Message
Package: github.com/xraph/herald/message
ID prefix: hmsg_
A Message represents a delivery log entry for a sent or queued notification. Every call to Herald.Send creates a message record that tracks the full lifecycle of the delivery attempt.
type Message struct {
ID id.MessageID `json:"id"`
AppID string `json:"app_id"`
EnvID string `json:"env_id,omitempty"`
TemplateID string `json:"template_id,omitempty"`
ProviderID string `json:"provider_id,omitempty"`
Channel string `json:"channel"`
Recipient string `json:"recipient"`
Subject string `json:"subject,omitempty"`
Body string `json:"body,omitempty"`
Status Status `json:"status"`
Error string `json:"error,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Async bool `json:"async"`
Attempts int `json:"attempts"`
SentAt *time.Time `json:"sent_at,omitempty"`
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}| Field | Description |
|---|---|
EnvID | Optional environment identifier for environment-scoped delivery logs. |
TemplateID | The template slug used to render this message (empty for raw sends). |
ProviderID | The provider that handled delivery. |
Recipient | The destination address (email, phone number, device token, user ID). |
Body | Truncated to Config.TruncateBodyAt characters (default 4096). |
Status | Current delivery status (see lifecycle below). |
Error | Error message if delivery failed. |
Async | Whether the send was dispatched asynchronously. |
Attempts | Number of delivery attempts. |
Status lifecycle
Messages progress through a defined status lifecycle:
queued --> sending --> sent --> delivered
\-> failed
\-> bouncedconst (
StatusQueued Status = "queued"
StatusSending Status = "sending"
StatusSent Status = "sent"
StatusFailed Status = "failed"
StatusBounced Status = "bounced"
StatusDelivered Status = "delivered"
)| Status | Description |
|---|---|
queued | Message created but not yet dispatched to the driver. |
sending | Driver send in progress. |
sent | Driver confirmed acceptance (does not guarantee inbox delivery). |
failed | Driver returned an error. The Error field contains details. |
bounced | Delivery bounced (hard bounce from the receiving server). |
delivered | Confirmed delivery to the recipient (when provider supports delivery tracking). |
ListOptions
type ListOptions struct {
Channel string
Status Status
Limit int
Offset int
}Message store interface
type Store interface {
CreateMessage(ctx context.Context, m *Message) error
GetMessage(ctx context.Context, messageID id.MessageID) (*Message, error)
UpdateMessageStatus(ctx context.Context, messageID id.MessageID, status Status, errMsg string) error
ListMessages(ctx context.Context, appID string, opts ListOptions) ([]*Message, error)
}Notification (Inbox)
Package: github.com/xraph/herald/inbox
ID prefix: hinb_
A Notification is an in-app inbox item delivered to a specific user. When a message is sent on the "inapp" channel, Herald automatically creates both a Message (delivery log) and a Notification (user-facing inbox item).
type Notification struct {
ID id.InboxID `json:"id"`
AppID string `json:"app_id"`
EnvID string `json:"env_id,omitempty"`
UserID string `json:"user_id"`
Type string `json:"type"`
Title string `json:"title"`
Body string `json:"body,omitempty"`
ActionURL string `json:"action_url,omitempty"`
ImageURL string `json:"image_url,omitempty"`
Read bool `json:"read"`
ReadAt *time.Time `json:"read_at,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}| Field | Description |
|---|---|
UserID | The user this notification belongs to. |
Type | Notification type slug (matches the template slug used to send it). |
ActionURL | Optional deep link or URL the user can navigate to. |
ImageURL | Optional image or avatar URL for rich inbox display. |
Read | Whether the user has read this notification. |
ReadAt | Timestamp of when the notification was marked as read. |
ExpiresAt | Optional expiration time after which the notification is hidden. |
Inbox store interface
type Store interface {
CreateNotification(ctx context.Context, n *Notification) error
GetNotification(ctx context.Context, notifID id.InboxID) (*Notification, error)
DeleteNotification(ctx context.Context, notifID id.InboxID) error
MarkRead(ctx context.Context, notifID id.InboxID) error
MarkAllRead(ctx context.Context, appID string, userID string) error
UnreadCount(ctx context.Context, appID string, userID string) (int, error)
ListNotifications(ctx context.Context, appID string, userID string, limit, offset int) ([]*Notification, error)
}Preference
Package: github.com/xraph/herald/preference
ID prefix: hprf_
A Preference represents a user's notification opt-in/opt-out settings for an app. Preferences are keyed by notification type slug and allow per-channel granularity.
type Preference struct {
ID id.PreferenceID `json:"id"`
AppID string `json:"app_id"`
UserID string `json:"user_id"`
Overrides map[string]ChannelPreference `json:"overrides"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}The Overrides map is keyed by notification type slug (e.g. "auth.welcome", "order.shipped"). Each value is a ChannelPreference that controls per-channel opt-in/opt-out:
type ChannelPreference struct {
Email *bool `json:"email,omitempty"`
SMS *bool `json:"sms,omitempty"`
Push *bool `json:"push,omitempty"`
InApp *bool `json:"inapp,omitempty"`
}A nil pointer means "use default" (opted in). An explicit false means the user has opted out of that channel for that notification type.
Checking opt-out status
The IsOptedOut method checks whether a user has explicitly opted out:
func (p *Preference) IsOptedOut(notifType string, channel string) boolpref, _ := store.GetPreference(ctx, "myapp", "user-42")
if pref.IsOptedOut("marketing.newsletter", "email") {
// User opted out of marketing emails -- skip delivery.
}Herald checks preferences automatically during Send. If a user has opted out, the send is silently skipped and a result with "user opted out" is returned.
Preference store interface
type Store interface {
GetPreference(ctx context.Context, appID string, userID string) (*Preference, error)
SetPreference(ctx context.Context, p *Preference) error
DeletePreference(ctx context.Context, appID string, userID string) error
}Scoped Config
Package: github.com/xraph/herald/scope
ID prefix: hscf_
A Config (scoped configuration) defines per-scope provider and sender overrides. Scoped configs allow different parts of your application (app-wide, per-organization, per-user) to use different providers and sender addresses.
type Config struct {
ID id.ScopedConfigID `json:"id"`
AppID string `json:"app_id"`
Scope ScopeType `json:"scope"`
ScopeID string `json:"scope_id"`
EmailProviderID string `json:"email_provider_id,omitempty"`
SMSProviderID string `json:"sms_provider_id,omitempty"`
PushProviderID string `json:"push_provider_id,omitempty"`
FromEmail string `json:"from_email,omitempty"`
FromName string `json:"from_name,omitempty"`
FromPhone string `json:"from_phone,omitempty"`
DefaultLocale string `json:"default_locale,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}Scope types
const (
ScopeApp ScopeType = "app"
ScopeOrg ScopeType = "org"
ScopeUser ScopeType = "user"
)| Field | Description |
|---|---|
Scope | The level at which this config applies: "app", "org", or "user". |
ScopeID | The identifier for this scope (app ID, org ID, or user ID). |
EmailProviderID | Override provider for the email channel. |
SMSProviderID | Override provider for the SMS channel. |
PushProviderID | Override provider for the push channel. |
FromEmail | Override sender email address. |
FromName | Override sender display name. |
FromPhone | Override sender phone number (SMS). |
DefaultLocale | Override default locale for template rendering. |
The ProviderIDFor method returns the provider ID override for a given channel:
func (c *Config) ProviderIDFor(channel string) stringScoped config store interface
type Store interface {
GetScopedConfig(ctx context.Context, appID string, scopeType ScopeType, scopeID string) (*Config, error)
SetScopedConfig(ctx context.Context, cfg *Config) error
DeleteScopedConfig(ctx context.Context, configID id.ScopedConfigID) error
ListScopedConfigs(ctx context.Context, appID string) ([]*Config, error)
}