Help us shape the future of Go! Take the 2026 Go Developer Survey

notifications

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 12 Imported by: 0

README

go-notifications

CI Go Reference

Send notifications from a Go SaaS application through email, SMS, push and an in-app inbox behind one API. A notification is a plain Go type that says how it renders on each channel; a Dispatcher picks the channels for each recipient, respects their opt-outs, delivers everywhere it should, and tells you exactly what happened per recipient and channel, including which push tokens to delete. SMTP, Twilio and Firebase Cloud Messaging are built in using only the standard library (plus golang.org/x/oauth2 for FCM), and the inbox is stored in PostgreSQL with pgx.

Install

go get github.com/byteminus/go-notifications

Requires Go 1.26 or later.

Quick start

package main

import (
	"context"
	"log"
	"os"

	"github.com/jackc/pgx/v5/pgxpool"

	notifications "github.com/byteminus/go-notifications"
	"github.com/byteminus/go-notifications/mail"
	"github.com/byteminus/go-notifications/pgxstore"
	"github.com/byteminus/go-notifications/sms"
	"github.com/byteminus/go-notifications/sms/twilio"
)

// User is your recipient model.
type User struct {
	ID, Name, Email, Phone string
}

func (u User) NotifiableRef() notifications.Ref { return notifications.Ref{Type: "user", ID: u.ID} }
func (u User) NotificationRoutes() notifications.Routes {
	return notifications.Routes{Name: u.Name, Email: u.Email, Phone: u.Phone}
}

// InvoicePaid goes out by email and SMS and lands in the in-app inbox.
type InvoicePaid struct{ Number, Amount string }

func (InvoicePaid) Type() string { return "invoice.paid" }

func (n InvoicePaid) ToMail(_ context.Context, _ notifications.Notifiable) (mail.Message, error) {
	return mail.Message{
		Subject: "Invoice " + n.Number + " paid",
		Text:    "We received " + n.Amount + ".",
		HTML:    "<p>We received <b>" + n.Amount + "</b>.</p>",
	}, nil
}

func (n InvoicePaid) ToSMS(_ context.Context, _ notifications.Notifiable) (sms.Message, error) {
	return sms.Message{Body: "Invoice " + n.Number + " paid"}, nil
}

func (n InvoicePaid) ToDatabase(_ context.Context, _ notifications.Notifiable) (notifications.DatabaseMessage, error) {
	return notifications.NewDatabaseMessage(map[string]string{"number": n.Number, "amount": n.Amount})
}

func main() {
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	store, err := pgxstore.New(pool)
	if err != nil {
		log.Fatal(err)
	}
	if err := store.CreateSchema(ctx); err != nil {
		log.Fatal(err)
	}
	mailer, err := mail.NewSMTP(mail.SMTPConfig{
		Host: "smtp.example.com", Username: os.Getenv("SMTP_USER"), Password: os.Getenv("SMTP_PASS"),
	})
	if err != nil {
		log.Fatal(err)
	}
	texts, err := twilio.New(twilio.Config{
		AccountSID: os.Getenv("TWILIO_ACCOUNT_SID"), AuthToken: os.Getenv("TWILIO_AUTH_TOKEN"), From: "+15005550006",
	})
	if err != nil {
		log.Fatal(err)
	}

	dispatcher, err := notifications.NewDispatcher(notifications.Config{
		Mailer:   mailer,
		MailFrom: mail.Address{Name: "Acme Billing", Email: "billing@acme.example"},
		SMS:      texts,
		Store:    store,
	})
	if err != nil {
		log.Fatal(err)
	}

	user := User{ID: "42", Name: "Ann", Email: "ann@example.com", Phone: "+14155550100"}
	report, err := dispatcher.Send(ctx, []notifications.Notifiable{user}, InvoicePaid{Number: "INV-7", Amount: "$120.00"})
	for _, r := range report.Results {
		log.Println(r.Recipient, r.Channel, r.Status, r.SkipReason)
	}
	if err != nil {
		log.Print(err) // every failure, joined; the other channels were still attempted
	}
}

Concepts

  • Notification: any type with Type() string, a stable name such as invoice.paid that is stored with inbox items and passed to preferences. It joins a channel by implementing ToMail, ToSMS, ToPush or ToDatabase, each of which renders a plain, JSON-serializable message.
  • Notifiable: the recipient. NotifiableRef identifies it with a typed Ref (for example {Type: "user", ID: "42"}, printed as user:42), so "user 42" and "team 42" never collide; NotificationRoutes returns its email address, E.164 phone number and push device tokens. A rendered message that names no recipient is addressed from these routes; a recipient with no route for a channel is skipped (SkipNoRoute), which is not an error.
  • Channel selection: by default a notification goes to every channel it implements and the dispatcher has configured. Implement Via(to) to choose per recipient; an explicit choice that cannot be honoured (a channel the notification lacks, one the dispatcher has no sender for, or an unknown name) is reported as a failure.
  • Preferences: Allows(ctx, to, type, channel) is asked before rendering. false skips the channel (SkipOptedOut); an error fails it, so an outage of the preference store never sends to someone who opted out. Allow security-critical types such as password resets unconditionally in your implementation.
  • Results: Send returns a Report with one Result per recipient and channel (sent, skipped or failed) and an error that joins every failure with errors.Join. Each failure is a *DeliveryError naming the notification, recipient and channel and wrapping the cause, so errors.Is(err, smtpErr) and errors.As(err, &twilioErr) work. One failure never stops the other channels or recipients. At most Config.Concurrency deliveries run at once; when the context is cancelled, deliveries not yet started fail with the context's error and Send returns once those in flight have returned.
  • Errors: every package exports ErrInvalid (and the root package also ErrNotFound and ErrConflict); the specific errors wrap them, so one errors.Is check per sentinel maps the whole package to HTTP statuses.
  • Invalid push tokens: tokens FCM reports as unregistered, belonging to another sender, or malformed are listed in Result.InvalidTokens and Report.InvalidTokens(). Delete them; they will never work again.
  • Inbox: the database channel stores a StoredNotification per recipient. Inbox lists them newest first in pages (DefaultPageSize, capped at MaxPageSize) with keyset pagination: pass Page.Next as ListOptions.Before until it is nil. It also counts unread ones, marks one or all as read, and deletes. Every operation is scoped to the owner Ref, which you take from the session: another user's notification ID returns ErrNotFound, exactly like an ID that does not exist.
  • Context values: this package reads no actor or tenant from the context, so there are no ActorFrom or TenantFrom hooks to wire. The recipient is always the explicit Notifiable or owner Ref.
Channels
Channel Message Interface Built-in implementation
mail mail.Message mail.Mailer mail.NewSMTP
sms sms.Message sms.Sender twilio.New (package sms/twilio)
push push.Message push.Sender fcm.New (package push/fcm)
database DatabaseMessage Store memstore.New, pgxstore.New

SMTP. Port 465 uses implicit TLS; any other port requires STARTTLS and refuses to continue if the server does not offer it, so by default credentials and content never travel in plaintext. Authentication is AUTH PLAIN. Messages are multipart/alternative when both text and HTML are set, bodies are quoted-printable, non-ASCII subjects, display names and custom headers are RFC 2047 encoded and folded, and any CR or LF in a header value is rejected with mail.ErrHeaderInjection before a connection is opened. Header lines are folded and never exceed 998 octets; text that merely looks like an encoded-word is encoded so readers show it literally. Credentials are sent only after TLS is established, the server certificate is always verified (InsecureSkipVerify is rejected; add a private CA to RootCAs), and the password never appears in errors or in a formatted SMTPConfig. For a local relay or a development catcher without TLS (Postfix on localhost:25, Mailpit without a certificate), set AllowPlaintext: true explicitly; it is never inferred, and NewSMTP rejects it together with credentials, so AUTH is never sent in plaintext. Each message uses its own connection; DialTimeout bounds connecting, CommandTimeout bounds each command, and the context bounds the whole delivery.

Twilio. Plain net/http against the Messages resource, authenticated with an auth token or (recommended) an API key. Sends from From, or from a Messaging Service pool. API failures are *twilio.Error with Twilio's error code, RetryAfter and Retryable() (HTTP 429, error 20429 and 5xx). errors.Is(err, sms.ErrInvalidRecipient) reports a bad number (21211, 21614) and errors.Is(err, sms.ErrUnsubscribed) a recipient who replied STOP (21610).

FCM. HTTP v1 API with a service account key (only service_account credentials are accepted) or any oauth2.TokenSource. Access tokens are cached. Tokens are sent one request each, sequentially. Tokens FCM reports as UNREGISTERED, SENDER_ID_MISMATCH or INVALID_ARGUMENT are listed in Response.Invalid (the payload is validated against FCM's rules first, so INVALID_ARGUMENT points at the token). Other failures are *fcm.Error with the FCM error code, RetryAfter and Retryable() (QUOTA_EXCEEDED, UNAVAILABLE, INTERNAL). A caller's http.Client without a timeout is used through a copy with DefaultTimeout, for Twilio as well.

Asynchronous delivery

Send delivers inline. To deliver from your job system, either:

  1. enqueue the inputs (recipient IDs and the notification's fields) and call Send inside the job; or
  2. call Prepare in the request, enqueue each returned Delivery as JSON, and call Deliver in the worker, returning its error to trigger a retry.

The second form renders once (templates see the state at request time) and retries per channel, so a failing SMS is retried without resending the email. Enqueue inside the same database transaction as the write that caused the notification. Each Delivery carries a UUIDv7 that the database channel uses as the stored notification's ID, so a retried database delivery is stored exactly once. Preferences are evaluated by Prepare.

// In the request: render once and enqueue one job per delivery.
func enqueueInvoicePaid(ctx context.Context, d *notifications.Dispatcher, to []notifications.Notifiable, enqueue func([]byte) error) error {
	deliveries, report, err := d.Prepare(ctx, to, InvoicePaid{Number: "INV-7"})
	if err != nil {
		log.Print(err) // channels that failed while preparing; see report.Results
	}
	_ = report
	for _, delivery := range deliveries {
		payload, err := json.Marshal(delivery)
		if err != nil {
			return err
		}
		if err := enqueue(payload); err != nil {
			return err
		}
	}
	return nil
}

// In the worker: a non-nil error retries the job.
func deliverJob(ctx context.Context, d *notifications.Dispatcher, payload []byte) error {
	var delivery notifications.Delivery
	if err := json.Unmarshal(payload, &delivery); err != nil {
		return err
	}
	_, err := d.Deliver(ctx, delivery)
	return err
}

PostgreSQL

pgxstore keeps the inbox in one table. CreateSchema runs this idempotent DDL (shown for the default table name); copy it into your migrations if you prefer:

CREATE TABLE IF NOT EXISTS "notifications" (
	id uuid PRIMARY KEY,
	notifiable_type text NOT NULL CHECK (notifiable_type <> ''),
	notifiable_id text NOT NULL CHECK (notifiable_id <> ''),
	type text NOT NULL CHECK (type <> ''),
	data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'),
	read_at timestamptz,
	created_at timestamptz NOT NULL
);
CREATE INDEX IF NOT EXISTS "notifications_recipient_idx" ON "notifications" (notifiable_type, notifiable_id, created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS "notifications_unread_idx" ON "notifications" (notifiable_type, notifiable_id, created_at DESC, id DESC) WHERE read_at IS NULL;

The partial index serves unread counts and unread lists, which are the hot path of a notification bell. WithTable("app.inbox") changes the (optionally schema-qualified) table name; Schema() returns the DDL for it.

To store an in-app notification in the same transaction as your own write, give the store a WithConn resolver that returns the transaction carried in the context (nil means "use the pool"):

type txKey struct{}

func newTxStore(pool *pgxpool.Pool) (*pgxstore.Store, error) {
	return pgxstore.New(pool, pgxstore.WithConn(func(ctx context.Context) pgxstore.Querier {
		if tx, ok := ctx.Value(txKey{}).(pgx.Tx); ok {
			return tx
		}
		return nil // use the pool
	}))
}

func payAndNotify(ctx context.Context, pool *pgxpool.Pool, d *notifications.Dispatcher, delivery notifications.Delivery) error {
	tx, err := pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback(ctx) }()
	// ... your writes on tx ...
	if _, err := d.Deliver(context.WithValue(ctx, txKey{}, tx), delivery); err != nil {
		return err
	}
	return tx.Commit(ctx) // the notification is stored only if this commits
}

Only the database channel joins the transaction; email, SMS and push leave the process immediately, which is why they belong in a job enqueued in the transaction rather than in the transaction itself.

API overview

Package Contents
notifications Notification, ToMail, ToSMS, ToPush, ToDatabase, Via; Notifiable, Routes, Ref; Preferences, PreferencesFunc; Dispatcher (NewDispatcher, Send, Prepare, Deliver), Config, Notifier, Delivery; Report, Result, Status, SkipReason, DeliveryError; Store, StoredNotification, ListOptions, Cursor, Page, NewPage; Inbox; ErrNotFound, ErrInvalid, ErrConflict and the specific errors that wrap them
mail Message, Address, Header, Mailer, MailerFunc, SMTP, SMTPConfig, Security, ErrInvalid
sms Message, Receipt, Sender, SenderFunc, IsE164, ErrInvalid, ErrInvalidRecipient, ErrUnsubscribed
sms/twilio New, Config, Sender, Error
push Message, Response, Delivery, Failure, Sender, SenderFunc, MaxTokens, ErrInvalid
push/fcm New, Config, Sender, Error
memstore in-memory, non-transactional Store
pgxstore PostgreSQL Store, Querier, WithTable, WithConn, Schema, CreateSchema
storetest Run, the Store conformance suite
notificationstest recording Mailer, SMSSender, PushSender; fake Dispatcher

Testing

In application tests, plug the recording senders from notificationstest into a real Dispatcher to assert on rendered messages, or depend on the Notifier interface and use notificationstest.Dispatcher to assert only what was sent to whom:

// markPaid is application code that depends on the Notifier interface.
func markPaid(ctx context.Context, n notifications.Notifier, u User) error {
	_, err := n.Send(ctx, []notifications.Notifiable{u}, InvoicePaid{Number: "INV-7"})
	return err
}

func TestMarkPaidNotifies(t *testing.T) {
	fake := &notificationstest.Dispatcher{}
	if err := markPaid(context.Background(), fake, User{ID: "42"}); err != nil {
		t.Fatal(err)
	}
	if got := fake.SentTo(notifications.Ref{Type: "user", ID: "42"}, "invoice.paid"); len(got) != 1 {
		t.Fatalf("sent %d invoice.paid notifications", len(got))
	}
}

A custom Store should pass the conformance suite:

func TestStore(t *testing.T) {
	// Replace memstore.New with the constructor of your store.
	storetest.Run(t, func(t *testing.T) notifications.Store { return memstore.New() })
}

To run this module's tests, see CONTRIBUTING.md. PostgreSQL tests read TEST_DATABASE_URL. SMTP integration tests deliver to a real Mailpit server (see CONTRIBUTING.md); Twilio and FCM tests use in-process fakes that assert the documented request and error shapes, including the OAuth token exchange.

Roadmap

Not in v0.1.0, planned for later releases:

  • Mail attachments and inline images; SMTPUTF8 (non-ASCII email addresses).
  • SMTP connection reuse for bulk sends.
  • Provider adapters for SES, Postmark and SendGrid, and APNs direct.
  • FCM platform overrides (Android, APNs, web push options), topics and conditions, and concurrent per-token sends.
  • Built-in retries with backoff that honour Retry-After.
  • Delivery logging/audit table and per-provider rate limiting.
  • Digests and batching, scheduled notifications, and localisation helpers.
  • Inbox retention (pruning old read notifications) and archived state.

Stability

This package follows semantic versioning. Before v1.0.0, a minor release may contain breaking changes; each one is listed in CHANGELOG.md with upgrade steps, including SQL for schema changes. From v1.0.0, the exported API and the PostgreSQL schema change only in backwards-compatible ways until the next major version.

License

MIT; see LICENSE.

Documentation

Overview

Package notifications sends notifications from SaaS applications through email, SMS, push and an in-app database inbox behind one API.

A notification is a type with a stable name that implements one interface per channel it supports (ToMail, ToSMS, ToPush, ToDatabase). A recipient implements Notifiable, which provides its identity as a typed Ref and its Routes: email address, E.164 phone number and push device tokens. A Dispatcher selects the channels for each recipient, asks Preferences whether the recipient has opted out, renders the messages and delivers them:

rep, err := dispatcher.Send(ctx, []notifications.Notifiable{user}, InvoicePaid{Invoice: inv})

Every recipient and channel is attempted even when others fail. The Report has one Result per recipient and channel; the error joins the failures with errors.Join. Push tokens the provider reports as invalid are listed in the results (Report.InvalidTokens) so the application can delete them.

Channels

Senders live in subpackages so that applications import only the providers they use: mail (with a built-in SMTP Mailer), sms with sms/twilio, and push with push/fcm. The database channel stores notifications through a Store; memstore and pgxstore implement it and Inbox provides the read side (paged list, unread count, mark read, delete), scoped to the owner's Ref so that one user cannot reach another user's notifications by ID.

Errors

Errors wrap the family sentinels ErrNotFound, ErrInvalid and ErrConflict, so an application maps them to HTTP statuses with errors.Is.

Asynchronous delivery

Send delivers inline. To deliver from a job queue, either enqueue the inputs (recipient IDs and the notification's fields) and call Send in the job, or call Prepare in the request, enqueue each returned Delivery as JSON, and call Deliver from the worker. The second form renders once and retries per channel: a failed SMS is retried without resending the email. Prepare assigns each Delivery an ID that the database channel reuses as the stored notification's ID, so a retried database delivery is stored once.

Testing

Package notificationstest provides recording senders and a Dispatcher fake that implements Notifier, and package storetest is a conformance suite for Store implementations.

Example
package main

import (
	"context"
	"fmt"
	"log"

	notifications "github.com/byteminus/go-notifications"
	"github.com/byteminus/go-notifications/mail"
	"github.com/byteminus/go-notifications/memstore"
	"github.com/byteminus/go-notifications/notificationstest"
	"github.com/byteminus/go-notifications/sms"
)

// Account is an application's user model.
type Account struct {
	ID    string
	Name  string
	Email string
	Phone string
}

func (a Account) NotifiableRef() notifications.Ref {
	return notifications.Ref{Type: "account", ID: a.ID}
}
func (a Account) NotificationRoutes() notifications.Routes {
	return notifications.Routes{Name: a.Name, Email: a.Email, Phone: a.Phone}
}

// InvoicePaid is sent by email and SMS and kept in the in-app inbox.
type InvoicePaid struct {
	Number string
	Amount string
}

func (InvoicePaid) Type() string { return "invoice.paid" }

func (n InvoicePaid) ToMail(_ context.Context, to notifications.Notifiable) (mail.Message, error) {
	return mail.Message{
		Subject: "Invoice " + n.Number + " paid",
		Text:    "We received " + n.Amount + ". Thank you.",
		HTML:    "<p>We received <strong>" + n.Amount + "</strong>. Thank you.</p>",
	}, nil
}

func (n InvoicePaid) ToSMS(context.Context, notifications.Notifiable) (sms.Message, error) {
	return sms.Message{Body: "Invoice " + n.Number + " paid: " + n.Amount}, nil
}

func (n InvoicePaid) ToDatabase(context.Context, notifications.Notifiable) (notifications.DatabaseMessage, error) {
	return notifications.NewDatabaseMessage(map[string]string{"number": n.Number, "amount": n.Amount})
}

func main() {
	mailer := &notificationstest.Mailer{} // use mail.NewSMTP in production
	texts := &notificationstest.SMSSender{}
	store := memstore.New() // use pgxstore in production

	d, err := notifications.NewDispatcher(notifications.Config{
		Mailer:   mailer,
		MailFrom: mail.Address{Name: "Acme Billing", Email: "billing@acme.example"},
		SMS:      texts,
		Store:    store,
		// Ann has turned off SMS for invoices.
		Preferences: notifications.PreferencesFunc(func(_ context.Context, to notifications.Notifiable, typ string, ch notifications.Channel) (bool, error) {
			return to.NotifiableRef().ID != "ann" || ch != notifications.ChannelSMS, nil
		}),
	})
	if err != nil {
		log.Fatal(err)
	}

	recipients := []notifications.Notifiable{
		Account{ID: "ann", Name: "Ann", Email: "ann@example.com", Phone: "+14155550100"},
		Account{ID: "bob", Name: "Bob", Email: "bob@example.com"},
	}
	rep, err := d.Send(context.Background(), recipients, InvoicePaid{Number: "INV-7", Amount: "$120.00"})
	if err != nil {
		log.Fatal(err)
	}
	for _, r := range rep.Results {
		line := fmt.Sprintf("%s %s %s", r.Recipient, r.Channel, r.Status)
		if r.SkipReason != "" {
			line += " " + string(r.SkipReason)
		}
		fmt.Println(line)
	}
	fmt.Println("emails:", len(mailer.Messages()), "first to:", mailer.Messages()[0].To[0])
}
Output:
account:ann mail sent
account:ann sms skipped opted_out
account:ann database sent
account:bob mail sent
account:bob sms skipped no_route
account:bob database sent
emails: 2 first to: "Ann" <ann@example.com>

Index

Examples

Constants

View Source
const (
	DefaultPageSize = 50
	MaxPageSize     = 200
)

Default and maximum page sizes for Store.List and Inbox.List.

View Source
const MaxDataSize = 64 << 10

MaxDataSize is the largest DatabaseMessage.Data, in bytes, that the database channel and every Store accept. In-app notifications carry references and short strings; the bound keeps a runaway payload from being stored once per recipient.

Variables

View Source
var (
	// ErrNotFound is returned when a stored notification does not exist or
	// belongs to another recipient. The two cases are indistinguishable on
	// purpose, so callers cannot probe other recipients' inboxes.
	ErrNotFound = errors.New("notifications: notification not found")

	// ErrInvalid is wrapped by every validation error: the input or the
	// configuration cannot be used as given.
	ErrInvalid = errors.New("notifications: invalid input")

	// ErrConflict is wrapped by errors that report a uniqueness violation.
	ErrConflict = errors.New("notifications: conflict")
)

The family sentinels. Every error this package returns that has one of these meanings wraps the matching sentinel, so an application maps them to HTTP statuses with errors.Is regardless of the more specific error below.

View Source
var (
	// ErrInvalidNotification is returned (wrapped) for a nil notification,
	// an empty notification type, or an invalid stored notification. It
	// wraps ErrInvalid.
	ErrInvalidNotification = newKindError("notifications: invalid notification", ErrInvalid)

	// ErrInvalidRecipient is returned (wrapped) for a nil recipient or a Ref
	// without both a type and an ID. It wraps ErrInvalid.
	ErrInvalidRecipient = newKindError("notifications: invalid recipient", ErrInvalid)

	// ErrUnknownChannel is reported for a channel name that is not built in.
	// It wraps ErrInvalid.
	ErrUnknownChannel = newKindError("notifications: unknown channel", ErrInvalid)

	// ErrUnsupportedChannel is reported when Via selects a channel the
	// notification does not implement. It wraps ErrInvalid.
	ErrUnsupportedChannel = newKindError("notifications: notification does not support channel", ErrInvalid)

	// ErrChannelNotConfigured is reported when a channel is selected but the
	// Dispatcher has no sender (or store) for it. It wraps ErrInvalid.
	ErrChannelNotConfigured = newKindError("notifications: channel not configured", ErrInvalid)

	// ErrInvalidDelivery is returned (wrapped) by Deliver for a Delivery
	// whose payload does not match its channel. It wraps ErrInvalid.
	ErrInvalidDelivery = newKindError("notifications: invalid delivery", ErrInvalid)

	// ErrInvalidConfig is returned (wrapped) by NewDispatcher and NewInbox.
	// It wraps ErrInvalid.
	ErrInvalidConfig = newKindError("notifications: invalid configuration", ErrInvalid)

	// ErrDuplicateID is returned by Store.Insert when the ID already exists.
	// It wraps ErrConflict.
	ErrDuplicateID = newKindError("notifications: duplicate notification id", ErrConflict)

	// ErrInvalidListOptions is returned (wrapped) for a malformed cursor or a
	// negative limit. It wraps ErrInvalid.
	ErrInvalidListOptions = newKindError("notifications: invalid list options", ErrInvalid)
)

Specific errors. Each wraps one of the family sentinels.

Functions

This section is empty.

Types

type Channel

type Channel string

Channel names a delivery channel.

const (
	ChannelMail     Channel = "mail"
	ChannelSMS      Channel = "sms"
	ChannelPush     Channel = "push"
	ChannelDatabase Channel = "database"
)

The built-in channels.

func (Channel) Valid

func (c Channel) Valid() bool

Valid reports whether c is one of the built-in channels.

type Config

type Config struct {
	// Mailer enables the mail channel.
	Mailer mail.Mailer
	// MailFrom is used for messages that do not set From. A zero address
	// leaves From empty, and such messages fail validation in the Mailer.
	MailFrom mail.Address
	// SMS enables the sms channel.
	SMS sms.Sender
	// Push enables the push channel.
	Push push.Sender
	// Store enables the database channel.
	Store Store
	// Preferences, when set, is consulted for every recipient and channel
	// before rendering.
	Preferences Preferences
	// Concurrency is the most deliveries Send runs at once. Zero means
	// one: deliveries run sequentially. Negative values are invalid.
	Concurrency int
	// Now stamps database notifications. Nil means time.Now.
	Now func() time.Time
}

Config configures a Dispatcher. A channel is enabled by setting its sender (or Store for the database channel); at least one is required. Nil fields disable their channel or feature.

type Cursor

type Cursor struct {
	CreatedAt time.Time `json:"created_at"`
	ID        string    `json:"id"`
}

Cursor is a keyset pagination position: List returns notifications older than it in (CreatedAt, ID) order. Keyset pagination keeps pages stable while new notifications arrive. It marshals to JSON so that an API can hand it to clients; treat a cursor from a client as untrusted, which ListOptions.Validate does.

type DatabaseMessage

type DatabaseMessage struct {
	Data json.RawMessage `json:"data"`
}

DatabaseMessage is the payload stored for an in-app notification. Data must be a JSON object of at most MaxDataSize bytes without \u0000 or unpaired surrogate escapes, which PostgreSQL's jsonb cannot store.

func NewDatabaseMessage

func NewDatabaseMessage(v any) (DatabaseMessage, error)

NewDatabaseMessage marshals v, which must encode as a JSON object of at most MaxDataSize bytes, into a DatabaseMessage. Errors wrap ErrInvalidNotification.

type Delivery

type Delivery struct {
	// ID is a UUIDv7 assigned by Prepare. The database channel uses it as
	// the stored notification's ID, which makes redelivery of the same
	// Delivery idempotent.
	ID string `json:"id"`
	// NotificationType is the Type of the notification that was rendered.
	NotificationType string `json:"notification_type"`
	// Recipient identifies who the delivery is for.
	Recipient Ref `json:"recipient"`
	// Channel selects which one of the payload fields below is set.
	Channel  Channel          `json:"channel"`
	Mail     *mail.Message    `json:"mail,omitempty"`
	SMS      *sms.Message     `json:"sms,omitempty"`
	Push     *push.Message    `json:"push,omitempty"`
	Database *DatabaseMessage `json:"database,omitempty"`
}

Delivery is one rendered message for one recipient on one channel. It is JSON-serializable so that it can be queued and delivered later by a worker calling Deliver; exactly one payload field is set, matching Channel.

func (Delivery) Validate

func (d Delivery) Validate() error

Validate reports whether d is well formed: an ID, a notification type, a valid recipient, and exactly the payload its channel needs, with valid data for the database channel. Errors wrap ErrInvalidDelivery. Message contents are validated by the sender when the Delivery is delivered.

type DeliveryError

type DeliveryError struct {
	// NotificationType, Recipient and Channel say what failed; Channel is
	// empty for an invalid recipient.
	NotificationType string
	Recipient        Ref
	Channel          Channel
	// Err is the cause.
	Err error
}

DeliveryError describes a failed channel. Unwrap exposes the cause, so errors.Is works with provider and package sentinels alike.

func (*DeliveryError) Error

func (e *DeliveryError) Error() string

Error implements error.

func (*DeliveryError) Unwrap

func (e *DeliveryError) Unwrap() error

Unwrap returns the cause.

type Dispatcher

type Dispatcher struct {
	// contains filtered or unexported fields
}

Dispatcher renders notifications and delivers them on every selected channel. It is safe for concurrent use.

func NewDispatcher

func NewDispatcher(cfg Config) (*Dispatcher, error)

NewDispatcher validates cfg and returns a Dispatcher. It returns an error wrapping ErrInvalidConfig when no channel is configured or Concurrency is negative.

func (*Dispatcher) Deliver

func (d *Dispatcher) Deliver(ctx context.Context, del Delivery) (Result, error)

Deliver sends one prepared Delivery and reports the outcome. The returned error is the Result's Err, a *DeliveryError wrapping ErrInvalidDelivery, ErrChannelNotConfigured, the context's error or the sender's error. Delivering the same database Delivery twice stores it once.

func (*Dispatcher) Prepare

func (d *Dispatcher) Prepare(ctx context.Context, recipients []Notifiable, n Notification) ([]Delivery, Report, error)

Prepare selects channels, applies Preferences and renders n for every recipient without delivering anything. It returns the deliveries to make and a Report of the channels that were skipped or failed while preparing. Enqueue the deliveries (they marshal to JSON) and call Deliver from a worker to send asynchronously. Preferences are evaluated now, not at delivery time. It returns an error with no deliveries only when n itself is invalid; otherwise the error joins the failures in the Report.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	notifications "github.com/byteminus/go-notifications"
	"github.com/byteminus/go-notifications/mail"
	"github.com/byteminus/go-notifications/memstore"
	"github.com/byteminus/go-notifications/notificationstest"
	"github.com/byteminus/go-notifications/sms"
)

// Account is an application's user model.
type Account struct {
	ID    string
	Name  string
	Email string
	Phone string
}

func (a Account) NotifiableRef() notifications.Ref {
	return notifications.Ref{Type: "account", ID: a.ID}
}
func (a Account) NotificationRoutes() notifications.Routes {
	return notifications.Routes{Name: a.Name, Email: a.Email, Phone: a.Phone}
}

// InvoicePaid is sent by email and SMS and kept in the in-app inbox.
type InvoicePaid struct {
	Number string
	Amount string
}

func (InvoicePaid) Type() string { return "invoice.paid" }

func (n InvoicePaid) ToMail(_ context.Context, to notifications.Notifiable) (mail.Message, error) {
	return mail.Message{
		Subject: "Invoice " + n.Number + " paid",
		Text:    "We received " + n.Amount + ". Thank you.",
		HTML:    "<p>We received <strong>" + n.Amount + "</strong>. Thank you.</p>",
	}, nil
}

func (n InvoicePaid) ToSMS(context.Context, notifications.Notifiable) (sms.Message, error) {
	return sms.Message{Body: "Invoice " + n.Number + " paid: " + n.Amount}, nil
}

func (n InvoicePaid) ToDatabase(context.Context, notifications.Notifiable) (notifications.DatabaseMessage, error) {
	return notifications.NewDatabaseMessage(map[string]string{"number": n.Number, "amount": n.Amount})
}

func main() {
	d, err := notifications.NewDispatcher(notifications.Config{
		Mailer:   &notificationstest.Mailer{},
		MailFrom: mail.Address{Email: "billing@acme.example"},
		Store:    memstore.New(),
	})
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()
	ann := Account{ID: "ann", Email: "ann@example.com"}

	// In the request: render now and enqueue one job per delivery, ideally
	// in the same transaction as the write that caused the notification.
	deliveries, _, err := d.Prepare(ctx, []notifications.Notifiable{ann}, InvoicePaid{Number: "INV-8", Amount: "$5.00"})
	if err != nil {
		log.Fatal(err)
	}
	var queue [][]byte
	for _, del := range deliveries {
		payload, err := json.Marshal(del)
		if err != nil {
			log.Fatal(err)
		}
		queue = append(queue, payload)
	}

	// In the worker: decode and deliver; return the error to retry the job.
	for _, payload := range queue {
		var del notifications.Delivery
		if err := json.Unmarshal(payload, &del); err != nil {
			log.Fatal(err)
		}
		res, err := d.Deliver(ctx, del)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(res.Channel, res.Status)
	}
}
Output:
mail sent
database sent

func (*Dispatcher) Send

func (d *Dispatcher) Send(ctx context.Context, recipients []Notifiable, n Notification) (Report, error)

Send prepares n for every recipient and delivers it on every selected channel. A failure on one channel or recipient never stops the others. The Report holds one Result per recipient and channel; the error joins every failure with errors.Join and is nil when nothing failed. Skips (opt-outs, missing routes) are not failures.

At most Config.Concurrency deliveries run at once. When ctx is cancelled, deliveries not yet started fail with the context's error instead of starting, and Send returns once the deliveries in flight have returned.

Send returns an error with an empty Report only when n itself is invalid.

type Inbox

type Inbox struct {
	// contains filtered or unexported fields
}

Inbox is the read side of the database channel: what an application's notification bell and inbox page call. Every method is scoped to the owner Ref, which the application takes from the authenticated session, never from the request: an ID owned by someone else behaves as missing (ErrNotFound). It is safe for concurrent use when its Store is.

Example
package main

import (
	"context"
	"fmt"
	"log"

	notifications "github.com/byteminus/go-notifications"
	"github.com/byteminus/go-notifications/mail"
	"github.com/byteminus/go-notifications/memstore"
	"github.com/byteminus/go-notifications/sms"
)

// Account is an application's user model.
type Account struct {
	ID    string
	Name  string
	Email string
	Phone string
}

func (a Account) NotifiableRef() notifications.Ref {
	return notifications.Ref{Type: "account", ID: a.ID}
}
func (a Account) NotificationRoutes() notifications.Routes {
	return notifications.Routes{Name: a.Name, Email: a.Email, Phone: a.Phone}
}

// InvoicePaid is sent by email and SMS and kept in the in-app inbox.
type InvoicePaid struct {
	Number string
	Amount string
}

func (InvoicePaid) Type() string { return "invoice.paid" }

func (n InvoicePaid) ToMail(_ context.Context, to notifications.Notifiable) (mail.Message, error) {
	return mail.Message{
		Subject: "Invoice " + n.Number + " paid",
		Text:    "We received " + n.Amount + ". Thank you.",
		HTML:    "<p>We received <strong>" + n.Amount + "</strong>. Thank you.</p>",
	}, nil
}

func (n InvoicePaid) ToSMS(context.Context, notifications.Notifiable) (sms.Message, error) {
	return sms.Message{Body: "Invoice " + n.Number + " paid: " + n.Amount}, nil
}

func (n InvoicePaid) ToDatabase(context.Context, notifications.Notifiable) (notifications.DatabaseMessage, error) {
	return notifications.NewDatabaseMessage(map[string]string{"number": n.Number, "amount": n.Amount})
}

func main() {
	ctx := context.Background()
	store := memstore.New()
	d, err := notifications.NewDispatcher(notifications.Config{Store: store})
	if err != nil {
		log.Fatal(err)
	}
	ann := Account{ID: "ann"}
	for _, number := range []string{"INV-1", "INV-2"} {
		if _, err := d.Send(ctx, []notifications.Notifiable{ann}, InvoicePaid{Number: number, Amount: "$1"}); err != nil {
			log.Fatal(err)
		}
	}

	inbox, err := notifications.NewInbox(store, notifications.InboxOptions{})
	if err != nil {
		log.Fatal(err)
	}
	// The owner comes from the authenticated session, never from the
	// request, so an ID from the request cannot reach another inbox.
	me := ann.NotifiableRef()
	var newest string
	opts := notifications.ListOptions{Limit: 1}
	for {
		page, err := inbox.List(ctx, me, opts)
		if err != nil {
			log.Fatal(err)
		}
		for _, n := range page.Items {
			if newest == "" {
				newest = n.ID
			}
			fmt.Println(n.Type, string(n.Data))
		}
		if page.Next == nil {
			break
		}
		opts.Before = page.Next
	}
	if err := inbox.MarkRead(ctx, me, newest); err != nil {
		log.Fatal(err)
	}
	unread, err := inbox.UnreadCount(ctx, me)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("unread:", unread)
}
Output:
invoice.paid {"amount":"$1","number":"INV-2"}
invoice.paid {"amount":"$1","number":"INV-1"}
unread: 1

func NewInbox

func NewInbox(store Store, opts InboxOptions) (*Inbox, error)

NewInbox returns an Inbox over store. It returns an error wrapping ErrInvalidConfig when store is nil.

func (*Inbox) Delete

func (in *Inbox) Delete(ctx context.Context, owner Ref, id string) error

Delete removes one of owner's notifications. It returns ErrNotFound if owner has no notification with that ID.

func (*Inbox) List

func (in *Inbox) List(ctx context.Context, owner Ref, opts ListOptions) (Page, error)

List returns a page of owneowner's notifications, newest first. Page.Next is nil on the last page. Errors wrap ErrInvalidRecipient or ErrInvalidListOptions for bad input, or come from the Store.

func (*Inbox) MarkAllRead

func (in *Inbox) MarkAllRead(ctx context.Context, owner Ref) (int, error)

MarkAllRead marks all of owner's unread notifications as read and returns how many changed.

func (*Inbox) MarkRead

func (in *Inbox) MarkRead(ctx context.Context, owner Ref, id string) error

MarkRead marks one of owner's notifications as read. Marking an already read notification keeps its original read time and succeeds. It returns ErrNotFound if owner has no notification with that ID.

func (*Inbox) UnreadCount

func (in *Inbox) UnreadCount(ctx context.Context, owner Ref) (int, error)

UnreadCount returns the number of owner's unread notifications.

type InboxOptions

type InboxOptions struct {
	// Now stamps read times. Nil means time.Now.
	Now func() time.Time
}

InboxOptions configures an Inbox. The zero value is valid.

type ListOptions

type ListOptions struct {
	// OnlyUnread restricts the result to unread notifications.
	OnlyUnread bool
	// Limit is the page size: zero means DefaultPageSize and values above
	// MaxPageSize are capped.
	Limit int
	// Before, when set, returns only notifications after this position in
	// newest-first order; pass the previous Page.Next to get the next page.
	Before *Cursor
}

ListOptions filters and pages Store.List. The zero value lists the first DefaultPageSize notifications.

func (ListOptions) EffectiveLimit

func (o ListOptions) EffectiveLimit() int

EffectiveLimit returns the page size a Store should use.

func (ListOptions) Validate

func (o ListOptions) Validate() error

Validate reports whether the options are usable. Errors wrap ErrInvalidListOptions.

type Notifiable

type Notifiable interface {
	NotifiableRef() Ref
	NotificationRoutes() Routes
}

Notifiable is a recipient: usually a user, sometimes a team or an anonymous address. Applications implement it on their own models.

NotifiableRef identifies the recipient in the inbox and to Preferences; it must return a Ref with both fields set, otherwise the recipient fails with ErrInvalidRecipient. NotificationRoutes says where to reach it. Both are called once per Send or Prepare and must be safe for concurrent use when the Dispatcher is used concurrently.

type Notification

type Notification interface {
	Type() string
}

Notification is something to tell a recipient. Type is a stable name such as "invoice.paid": it is stored with database notifications and passed to Preferences, so renaming it is a breaking change for stored data and opt-outs.

A notification takes part in a channel by implementing the matching interface: ToMail, ToSMS, ToPush or ToDatabase. It may also implement Via to choose channels per recipient.

type Notifier

type Notifier interface {
	Send(ctx context.Context, recipients []Notifiable, n Notification) (Report, error)
}

Notifier sends a notification to recipients. Application code depends on it rather than on *Dispatcher so that tests can substitute notificationstest.Dispatcher. Applications call it and do not usually implement it; it may gain methods in a minor release.

type Page added in v0.2.0

type Page struct {
	// Items are the notifications, newest first.
	Items []StoredNotification
	// Next is the cursor for the following page, or nil when there are no
	// more notifications. Pass it as ListOptions.Before.
	Next *Cursor
}

Page is one page of a List.

func NewPage added in v0.2.0

func NewPage(rows []StoredNotification, opts ListOptions) Page

NewPage builds a Page for a Store that fetched up to limit+1 rows, where limit is opts.EffectiveLimit(): the extra row only signals that another page exists and is dropped.

type Preferences

type Preferences interface {
	Allows(ctx context.Context, to Notifiable, notificationType string, ch Channel) (bool, error)
}

Preferences decides whether a recipient accepts a notification type on a channel. Notifications that must always be delivered, such as password resets, should be allowed by the implementation regardless of opt-outs.

An error from Allows fails that channel rather than sending: when the preference store is unavailable the safe default is to respect a possible opt-out.

type PreferencesFunc

type PreferencesFunc func(ctx context.Context, to Notifiable, notificationType string, ch Channel) (bool, error)

PreferencesFunc adapts a function to the Preferences interface.

func (PreferencesFunc) Allows

func (f PreferencesFunc) Allows(ctx context.Context, to Notifiable, notificationType string, ch Channel) (bool, error)

Allows calls f.

type Ref added in v0.2.0

type Ref struct {
	Type string `json:"type"`
	ID   string `json:"id"`
}

Ref identifies a record or party by type and ID, such as a recipient ("user", "42") or an inbox owner. Compare Refs as structs; never use String as a key.

func (Ref) IsZero added in v0.2.0

func (r Ref) IsZero() bool

IsZero reports whether both fields are empty.

func (Ref) String added in v0.2.0

func (r Ref) String() string

String returns "type:id", for logs and display only.

func (Ref) Validate added in v0.2.0

func (r Ref) Validate() error

Validate reports whether r can identify a recipient: both fields set, valid UTF-8 without NUL bytes, and at most 255 bytes each. The error wraps ErrInvalidRecipient and so ErrInvalid. A zero Ref is invalid because every notification has a recipient. Store implementations call it on every Ref they receive.

type Report

type Report struct {
	// Results holds one Result per recipient and channel.
	Results []Result
}

Report collects the results of a Send or Prepare, ordered by recipient and then channel.

func (Report) Count

func (r Report) Count(s Status) int

Count returns the number of results with the given status.

func (Report) Err

func (r Report) Err() error

Err joins the errors of every failed result with errors.Join, or returns nil when nothing failed.

func (Report) Failed

func (r Report) Failed() []Result

Failed returns the failed results.

func (Report) InvalidTokens

func (r Report) InvalidTokens() map[Ref][]string

InvalidTokens groups the push tokens to delete by recipient.

type Result

type Result struct {
	// DeliveryID identifies the delivery; empty when the channel failed or
	// was skipped before a Delivery was prepared. For the database channel
	// it is also the stored notification's ID.
	DeliveryID string
	// NotificationType is the Type of the notification.
	NotificationType string
	// Recipient identifies who the result is for; it may be invalid when
	// the result reports an invalid recipient.
	Recipient Ref
	// Channel is empty only for a result reporting an invalid recipient.
	Channel Channel
	// Status is the outcome.
	Status Status
	// SkipReason says why a StatusSkipped result was skipped.
	SkipReason SkipReason
	// MessageID is the provider's identifier when it returns one (Twilio
	// SID, stored notification ID). Push results leave it empty because a
	// message fans out to several devices.
	MessageID string
	// InvalidTokens lists push device tokens the provider rejected as
	// unregistered or malformed; delete them from the recipient. It can be
	// non-empty on a sent result when other devices were reached.
	InvalidTokens []string
	// Err is a *DeliveryError when Status is StatusFailed, nil otherwise.
	// For push it may coexist with successful delivery to some devices.
	Err error
}

Result is the outcome of one channel for one recipient.

type Routes

type Routes struct {
	// Name is the display name used in email headers.
	Name string `json:"name,omitempty"`
	// Email is a bare address such as ann@example.com.
	Email string `json:"email,omitempty"`
	// Phone is an E.164 number such as +14155550100.
	Phone string `json:"phone,omitempty"`
	// PushTokens are the recipient's device tokens.
	PushTokens []string `json:"push_tokens,omitempty"`
}

Routes holds a recipient's addresses. Empty fields mean the recipient cannot be reached on that channel, and the channel is skipped.

type SkipReason

type SkipReason string

SkipReason explains a StatusSkipped result.

const (
	// SkipOptedOut means Preferences did not allow the channel.
	SkipOptedOut SkipReason = "opted_out"
	// SkipNoRoute means the recipient has no address for the channel and
	// the rendered message named none.
	SkipNoRoute SkipReason = "no_route"
)

The possible skip reasons.

type Status

type Status string

Status is the outcome of one channel for one recipient.

const (
	// StatusSent means the provider (or store) accepted the message.
	StatusSent Status = "sent"
	// StatusSkipped means nothing was attempted, for the reason given in
	// Result.SkipReason. A skip is not an error.
	StatusSkipped Status = "skipped"
	// StatusFailed means the attempt failed; Result.Err says why.
	StatusFailed Status = "failed"
)

The possible statuses.

type Store

type Store interface {
	// Insert stores n. It returns ErrDuplicateID if n.ID exists, whoever
	// owns it.
	Insert(ctx context.Context, n StoredNotification) error
	// List returns a page of owner's notifications, newest first, ordered by
	// (CreatedAt, ID) descending.
	List(ctx context.Context, owner Ref, opts ListOptions) (Page, error)
	// UnreadCount returns the number of owner's unread notifications.
	UnreadCount(ctx context.Context, owner Ref) (int, error)
	// MarkRead sets ReadAt to at unless it is already set, in which case the
	// original time is kept. It returns ErrNotFound if owner has no such
	// notification.
	MarkRead(ctx context.Context, owner Ref, id string, at time.Time) error
	// MarkAllRead marks every unread notification of owner as read at at
	// and returns how many changed.
	MarkAllRead(ctx context.Context, owner Ref, at time.Time) (int, error)
	// Delete removes one of owner's notifications. It returns ErrNotFound if
	// owner has no such notification.
	Delete(ctx context.Context, owner Ref, id string) error
}

Store persists in-app notifications. Applications usually use memstore or pgxstore; implement it only to keep the inbox in another database, and run the storetest conformance suite against the implementation.

Every method that reads or changes existing notifications takes the owning recipient and treats a notification owned by someone else as missing (ErrNotFound), so an ID taken from a request can never reach another recipient's inbox.

Implementations must be safe for concurrent use; validate their input with StoredNotification.Validate, Ref.Validate and ListOptions.Validate and return those errors unchanged; return times in UTC truncated to microseconds; and treat an ID that is not a UUID as not found. MarkRead must not overwrite a read time set concurrently: use a conditional update or a row lock, never an unguarded read-modify-write.

type StoredNotification

type StoredNotification struct {
	// ID is a UUID; stores return it in canonical lowercase form.
	ID string `json:"id"`
	// Recipient owns the notification.
	Recipient Ref `json:"recipient"`
	// Type is the notification type, such as "invoice.paid".
	Type string `json:"type"`
	// Data is the JSON object rendered by ToDatabase.
	Data json.RawMessage `json:"data"`
	// ReadAt is nil while the notification is unread.
	ReadAt *time.Time `json:"read_at"`
	// CreatedAt orders the inbox.
	CreatedAt time.Time `json:"created_at"`
}

StoredNotification is an in-app notification as kept by a Store.

func (StoredNotification) Cursor

func (n StoredNotification) Cursor() Cursor

Cursor returns the position after n, for fetching the next page.

func (StoredNotification) Unread

func (n StoredNotification) Unread() bool

Unread reports whether the notification has not been read.

func (StoredNotification) Validate

func (n StoredNotification) Validate() error

Validate reports whether n satisfies the constraints every Store enforces: a UUID ID, a valid recipient, a type of at most 255 bytes of UTF-8 without NUL, Data accepted by NewDatabaseMessage, and a creation time. Errors wrap ErrInvalidNotification or ErrInvalidRecipient, and so ErrInvalid.

type ToDatabase

type ToDatabase interface {
	ToDatabase(ctx context.Context, to Notifiable) (DatabaseMessage, error)
}

ToDatabase is implemented by notifications that are stored in the recipient's in-app inbox.

type ToMail

type ToMail interface {
	ToMail(ctx context.Context, to Notifiable) (mail.Message, error)
}

ToMail is implemented by notifications that can be sent by email. A message without recipients is addressed to the recipient's Routes.Email, and a message without From uses Config.MailFrom.

type ToPush

type ToPush interface {
	ToPush(ctx context.Context, to Notifiable) (push.Message, error)
}

ToPush is implemented by notifications that can be sent as push notifications. A message without Tokens is addressed to the recipient's Routes.PushTokens.

type ToSMS

type ToSMS interface {
	ToSMS(ctx context.Context, to Notifiable) (sms.Message, error)
}

ToSMS is implemented by notifications that can be sent by SMS. A message without To is addressed to the recipient's Routes.Phone.

type Via

type Via interface {
	Via(to Notifiable) []Channel
}

Via is implemented by notifications that choose their channels per recipient. Without it, a notification goes to every channel it implements and the Dispatcher has configured. Channels named by Via must be both implemented and configured, otherwise the result for that channel is a failure: an explicit choice that cannot be honoured is a programming error worth surfacing.

Directories

Path Synopsis
internal
httpx
Package httpx holds the HTTP details shared by the provider adapters.
Package httpx holds the HTTP details shared by the provider adapters.
Package mail defines the email message type used by go-notifications, the Mailer interface that provider adapters implement, and a built-in SMTP Mailer.
Package mail defines the email message type used by go-notifications, the Mailer interface that provider adapters implement, and a built-in SMTP Mailer.
Package memstore is an in-memory notifications.Store for tests, examples and single-process prototypes.
Package memstore is an in-memory notifications.Store for tests, examples and single-process prototypes.
Package notificationstest provides test doubles for applications that use go-notifications.
Package notificationstest provides test doubles for applications that use go-notifications.
Package pgxstore is a PostgreSQL notifications.Store built on pgx v5.
Package pgxstore is a PostgreSQL notifications.Store built on pgx v5.
Package push defines the push notification message type used by go-notifications and the Sender interface that provider adapters implement.
Package push defines the push notification message type used by go-notifications and the Sender interface that provider adapters implement.
fcm
Package fcm implements push.Sender over the Firebase Cloud Messaging HTTP v1 API, authenticating with a Google service account through golang.org/x/oauth2/google.
Package fcm implements push.Sender over the Firebase Cloud Messaging HTTP v1 API, authenticating with a Google service account through golang.org/x/oauth2/google.
sms
Package sms defines the SMS message type used by go-notifications and the Sender interface that provider adapters implement.
Package sms defines the SMS message type used by go-notifications and the Sender interface that provider adapters implement.
twilio
Package twilio implements sms.Sender over the Twilio Programmable Messaging REST API using only net/http.
Package twilio implements sms.Sender over the Twilio Programmable Messaging REST API using only net/http.
Package storetest is a conformance suite for notifications.Store implementations.
Package storetest is a conformance suite for notifications.Store implementations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL