Vault

Providers

Configure notification delivery providers with driver credentials, channel types, and priority-based selection.

Providers are the bridge between Herald and external notification services. Each provider binds a driver (SMTP, Resend, Twilio, FCM, or InApp) to a set of credentials and a channel type, giving Herald the information it needs to deliver notifications through that service.

Provider Entity

The provider.Provider struct represents a configured delivery provider scoped to a single application and channel:

package provider

type Provider struct {
    ID          id.ProviderID     `json:"id"`
    AppID       string            `json:"app_id"`
    Name        string            `json:"name"`
    Channel     string            `json:"channel"`       // "email", "sms", "push", "inapp"
    Driver      string            `json:"driver"`        // "smtp", "resend", "twilio", "fcm", "inapp"
    Credentials map[string]string `json:"credentials"`   // driver-specific secrets
    Settings    map[string]string `json:"settings"`      // driver-specific non-secret config
    Priority    int               `json:"priority"`      // lower value = higher priority
    Enabled     bool              `json:"enabled"`
    CreatedAt   time.Time         `json:"created_at"`
    UpdatedAt   time.Time         `json:"updated_at"`
}

Each provider is assigned a TypeID with the hpvd prefix (e.g., hpvd_01j8x7k2...).

Channel Types

Herald supports four notification channels. Each provider is bound to exactly one channel:

ChannelDescriptionCommon Drivers
emailEmail deliverysmtp, resend
smsSMS text messagestwilio
pushMobile push notificationsfcm
inappIn-app inbox notificationsinapp

The herald.ChannelType constants define these values:

const (
    ChannelEmail ChannelType = "email"
    ChannelSMS   ChannelType = "sms"
    ChannelPush  ChannelType = "push"
    ChannelInApp ChannelType = "inapp"
)

Driver Configuration

Each driver requires specific credentials and settings. These are stored as map[string]string on the provider.

SMTP

Credentials: map[string]string{
    "host":     "smtp.example.com",
    "port":     "587",
    "username": "notifications@example.com",
    "password": "secret",
},
Settings: map[string]string{
    "use_tls":   "true",
    "from":      "noreply@example.com",
    "from_name": "My App",
},

Resend

Credentials: map[string]string{
    "api_key": "re_abc123...",
},
Settings: map[string]string{
    "from":      "noreply@example.com",
    "from_name": "My App",
},

Twilio

Credentials: map[string]string{
    "account_sid": "AC...",
    "auth_token":  "auth_token_here",
    "from_number": "+15551234567",
},

FCM (Firebase Cloud Messaging)

Credentials: map[string]string{
    "project_id":   "my-firebase-project",
    "access_token": "ya29.a0...",      // OAuth2 access token
    // OR legacy:
    "server_key":   "AAAA...",         // legacy server key
},

InApp

The in-app driver requires no credentials. Herald handles inbox storage directly:

Credentials: map[string]string{},
Settings:    map[string]string{},

Priority-Based Selection

When multiple providers are configured for the same channel, Herald selects the best one using a priority system. Lower Priority values indicate higher preference.

During delivery, the scope resolver walks the scope chain (user, org, app) to find provider overrides. If no override exists, Herald falls back to the first enabled provider for that channel sorted by priority:

// Providers are sorted by priority (ascending) before selection.
sort.Slice(providers, func(i, j int) bool {
    return providers[i].Priority < providers[j].Priority
})

for _, p := range providers {
    if p.Enabled {
        return &ResolveResult{Provider: p}, nil
    }
}

This means you can set up primary and fallback providers:

ProviderChannelPriorityEnabled
Resend Productionemail0true
SMTP Fallbackemail10true
Twiliosms0true

Enabling and Disabling

Providers can be toggled on or off via the Enabled field. A disabled provider is skipped during resolution, allowing you to take a provider offline without deleting its configuration.

Store Interface

The provider.Store interface defines the persistence operations:

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)
}

ListProviders filters by channel while ListAllProviders returns every provider for the application regardless of channel.

API Endpoints

Herald exposes full CRUD for providers under the /providers route group:

MethodPathOperation
POST/providersCreate a new provider
GET/providersList providers (filter by app_id, channel)
GET/providers/:idGet a single provider
PUT/providers/:idUpdate a provider
DELETE/providers/:idDelete a provider

Create Provider Example

req := &api.CreateProviderRequest{
    AppID:   "app_01abc...",
    Name:    "Production Resend",
    Channel: "email",
    Driver:  "resend",
    Credentials: map[string]string{
        "api_key": "re_abc123...",
    },
    Settings: map[string]string{
        "from":      "noreply@myapp.com",
        "from_name": "MyApp Notifications",
    },
    Priority: 0,
    Enabled:  true,
}

Driver Validation

Each driver implements a Validate method that checks whether the provided credentials contain all required keys before the provider is used:

type Driver interface {
    Name() string
    Channel() string
    Send(ctx context.Context, msg *OutboundMessage) (*DeliveryResult, error)
    Validate(credentials, settings map[string]string) error
}

For example, the SMTP driver requires host and port, the Resend driver requires api_key, and the Twilio driver requires account_sid, auth_token, and from_number. See the Drivers page for the full list of required credentials per driver.

On this page