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

courier

package module
v0.0.12 Latest Latest
Warning

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

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

README

backplane-courier

Message delivery for a Backplane installation: mail, push, Web Push and any provider a plugin adds, through the installation's own channels. A product never imports it: it raises a hook or publishes an event, and the operator wires that to a courier activity in the console, usually after templates.Render.

Courier keeps nothing. Every activity delivers at once and answers; a temporary failure goes back to the caller, whose retry policy (backplane's, per step and per for-each item) decides; a final one is not retried; a provider's Retry-After becomes activity.RetryAfter. Every delivery is a platform audit record (courier.delivered, courier.failed; recipients and tokens hashed, never a payload). Encryption, fan-out, muting and the like are the product's and the installation's choices: courier exposes each provider's full semantics.

Activities

Activity What
courier.SendMail A rendered mail (to, cc, bcc, reply_to, subject, text, html, headers, attachments, optional from, channel) through a mail channel. Answers {channel, message_id}.
courier.SendPush A push to one token, topic or condition: title, body, image, data, priority, ttl_seconds, collapse_key, and the raw FCM android, apns, webpush, fcm_options blocks merged over them, so every FCM field is reachable (mutable-content, content-available, apns-push-type, ...). validate_only checks with FCM without delivering. Answers {channel, name}.
courier.Subscribe, courier.Unsubscribe Device tokens (at most 1000) to or from an FCM topic; answers each token's outcome.
courier.SendWebPush A Web Push to one browser subscription ({endpoint, keys: {p256dh, auth}}, as PushSubscription.toJSON() writes it): payload (any JSON, at most 3993 bytes, what the service worker gets as event.data.json()), ttl_seconds (default 86400), urgency, topic. Answers {channel, id}. A subscription the browser dropped fails for good with code gone: delete it.
courier.WebPushKey The channel's VAPID public key, the applicationServerKey browsers subscribe with.
courier.Send<Kind> One per provider kind a plugin or a compiled-in provider adds (SendSms), with that provider's message schema plus an optional channel.

A message names its channel, else the kind's default (defaults), else the kind's only channel.

Channels

channels:
  mail:
    smtp: {host: smtp.example.com, port: 587, tls: starttls, username: shop, password: ${SMTP_PASSWORD}, from: "Shop <no-reply@shop.example>"}
    pool: 8
    rate_per_minute: 600
    timeout: 30s
  push:
    fcm: {credentials: ${FCM_KEY}}      # service account key JSON; project_id from it unless set
  web:
    webpush: {subject: mailto:ops@shop.example, public_key: B..., private_key: ${VAPID_PRIVATE_KEY}}
  dev: {log: {kind: mail}}               # deliveries of a kind go to the log
  sms:
    provider: {name: smsc, settings: {sender: Shop}, secrets: {token: ${SMSC_TOKEN}}}
plugins:
  smsc:
    address: unix:///run/courier/smsc.sock   # or host:port
    tls: {enabled: false}                    # TLS or mTLS to a remote plugin
    startup_timeout: 30s
defaults: {mail: mail}                 # needed only when a kind has several channels
dry_run: false                         # live: record deliveries, send nothing

Web Push

Web Push is a standard (RFC 8030, 8291, 8292), so it is built in: no vendor account, one provider for Chrome, Edge, Firefox and Safari, whose push services the subscription's endpoint names. A webpush channel is a VAPID key pair and a contact subject (mailto: or an https URL); generate the pair in the console (Channels → Web Push) and keep the private key a secret. The console also sends a test push to a subscription pasted from a browser.

Courier keeps no subscriptions: the product stores what its pages hand it, fans out per device, and deletes a subscription when a push fails with code gone. A subscription comes from a browser and is untrusted input, so courier posts only over https to the hosts in hosts (default: the browsers' own push services: *.googleapis.com, *.push.services.mozilla.com, *.push.apple.com, *.notify.windows.com), never to an IP address, and follows no redirect.

Providers courier does not know

A plugin is a gRPC service implementing courier.plugin.v1.ProviderService (proto/courier/plugin/v1): Describe (its kind, the schemas of a channel's settings, of a message and of a result), Check and Send. In Go, implement provider.Provider and call plugin.Serve(address, p) (examples/smsplugin); in any other language, implement the proto. Where and how the plugin runs (a sidecar, another container, a process the image starts) is the deployment's concern: courier dials its address. Courier waits for it at startup up to startup_timeout, probes its standard gRPC health as part of its own readiness, keeps the connection alive and lets gRPC reconnect. The contract evolves as protobuf does; an incompatible change is a new package.

A compiled-in provider is a provider.Provider built into a custom binary:

func main() { courier.Main(courier.WithProvider("smsc", smsc.New())) }

Either way the provider's channels use {provider: {name: ...}}, courier declares courier.Send<Kind> with the provider's schema, and a provider.Error says whether a failure is final or when to retry.

Development

make configure          # linter and proto generators into ./bin
make configure-web      # the console module's packages (GitHub Packages)
make gen                # Go code of the protos and the module's TypeScript client
make test
make web                # build the console module (embedded by make build)
go run ./examples/smsplugin &   # the example plugin of deployments/dev.yaml
make run                # against the local stack with deployments/dev.yaml

Tests run fake SMTP and FCM endpoints in process (internal/smtp/smtptest, internal/fcm/fcmtest) and a plugin over a unix socket.

Documentation

Overview

Package courier is the courier service as a library: Main runs it, with the providers a custom build compiles in.

func main() { courier.Main(courier.WithProvider("smsc", smsc.New())) }

The stock binary (cmd/courier) is Main with nothing added. A provider in another language runs as a plugin (package plugin) the configuration names by address.

Index

Constants

This section is empty.

Variables

View Source
var ErrContractChanged = errors.New("changed its contract since courier started: restart courier to take it")

ErrContractChanged: the plugin now describes another kind or other schemas than courier declared at startup.

View Source
var ErrProviderName = errors.New("courier: provider name used twice")

ErrProviderName: a provider name is used twice.

Functions

func Main

func Main(opts ...Option)

Main runs courier until a signal stops it, exiting the process on error.

func Run

func Run(ctx context.Context, opts ...Option) error

Run runs courier until ctx ends or a signal stops it.

Types

type Config

type Config struct {
	config.Backplane `json:"backplane"`

	// Channels by name; each has exactly one provider.
	Channels channels.Config `json:"channels"`
	// Plugins by name: providers courier dials.
	Plugins map[string]channels.Plugin `json:"plugins,omitempty"`
	// Defaults: the channel of a kind (mail, push, a provider's) when a
	// message names none; a kind with one channel needs no default.
	Defaults map[string]string `json:"defaults,omitempty"`
	// DryRun records deliveries without sending them; live.
	DryRun config.Live[bool] `json:"dry_run" schemapb:"default=false;description=Record deliveries, send nothing"`
	// MaxMessageBytes bounds a mail's bodies and attachments.
	MaxMessageBytes int64 `json:"max_message_bytes" schemapb:"default=26214400;gte=4096"`
	// MaxRecipients bounds a mail's to + cc + bcc.
	MaxRecipients int64 `json:"max_recipients" schemapb:"default=100;gte=1;lte=1000"`
}

Config is the whole configuration: file (BACKPLANE_CONFIG_FILE) < env COURIER_* (BACKPLANE_* for the SDK block) < Consul KV for Live fields.

func (*Config) Validate

func (c *Config) Validate() error

Validate what the schema cannot.

type Option

type Option func(*options)

Option configures Main.

func WithProvider

func WithProvider(name string, p provider.Provider) Option

WithProvider compiles p in under name: channels use it as {provider: {name: <name>}}.

func WithTokenSource

func WithTokenSource(ts oauth2.TokenSource) Option

WithTokenSource authorizes every FCM channel with ts instead of its credentials (tests, a sidecar issuing tokens).

type State

type State struct {
	Channels *channels.Channels
	Courier  *core.Courier
	Admin    *admin.Admin
	// contains filtered or unexported fields
}

State is the service's tree.

Directories

Path Synopsis
cmd
courier command
courier is a Backplane tool service: it delivers rendered messages — mail, push, and whatever a provider plugin adds — through the channels of an installation.
courier is a Backplane tool service: it delivers rendered messages — mail, push, and whatever a provider plugin adds — through the channels of an installation.
examples
smsplugin command
smsplugin is an example courier plugin: an SMS "gateway" that logs what it would send.
smsplugin is an example courier plugin: an SMS "gateway" that logs what it would send.
internal
admin
Package admin is courier's internal API: the console module's channel list and check, served on the platform port behind the internal secret.
Package admin is courier's internal API: the console module's channel list and check, served on the platform port behind the internal secret.
channels
Package channels holds the installation's channels: their configuration (exactly one provider each), the runtime of each (pool, rate, timeout) and which channel a kind uses by default.
Package channels holds the installation's channels: their configuration (exactly one provider each), the runtime of each (pool, rate, timeout) and which channel a kind uses by default.
courier
Package courier is the service's work: an activity per kind of delivery — SendMail, SendPush (with Subscribe and Unsubscribe for FCM topics), SendWebPush (with WebPushKey for the browsers) and Send<Kind> for every extension provider — each delivering at once through the channel the message names or the kind's default.
Package courier is the service's work: an activity per kind of delivery — SendMail, SendPush (with Subscribe and Unsubscribe for FCM topics), SendWebPush (with WebPushKey for the browsers) and Send<Kind> for every extension provider — each delivering at once through the channel the message names or the kind's default.
fcm
Package fcm is the built-in push provider: FCM HTTP v1 per channel (a Firebase project) with OAuth2 from a service account, and topic subscriptions through the Instance ID API.
Package fcm is the built-in push provider: FCM HTTP v1 per channel (a Firebase project) with OAuth2 from a service account, and topic subscriptions through the Instance ID API.
fcm/fcmtest
Package fcmtest is a fake FCM endpoint for tests (HTTP v1 sends and Instance ID subscriptions): it records what it receives and answers by rules on the target.
Package fcmtest is a fake FCM endpoint for tests (HTTP v1 sends and Instance ID subscriptions): it records what it receives and answers by rules on the target.
mail
Package mail is the service's domain: the message a caller hands to courier.SendMail, its checks and limits, address hashing and the MIME it becomes.
Package mail is the service's domain: the message a caller hands to courier.SendMail, its checks and limits, address hashing and the MIME it becomes.
push
Package push is the push domain: the message a caller hands to courier.SendPush, its checks, token hashing and the FCM HTTP v1 message it becomes.
Package push is the push domain: the message a caller hands to courier.SendPush, its checks, token hashing and the FCM HTTP v1 message it becomes.
smtp
Package smtp is the built-in mail provider: one SMTP relay per channel, a connection per delivery, 5xx replies final.
Package smtp is the built-in mail provider: one SMTP relay per channel, a connection per delivery, 5xx replies final.
smtp/smtptest
Package smtptest is an SMTP server for tests: it keeps what it receives and refuses the recipients it is told to.
Package smtptest is an SMTP server for tests: it keeps what it receives and refuses the recipients it is told to.
ui
Package ui carries the console module: web/dist/plugin copied here by `make web` and embedded into the binary.
Package ui carries the console module: web/dist/plugin copied here by `make web` and embedded into the binary.
webpush
Package webpush is the built-in Web Push provider: the standard protocol every browser's push service speaks (RFC 8030), the payload encrypted to the subscription (RFC 8291) and the sender identified by its VAPID key (RFC 8292).
Package webpush is the built-in Web Push provider: the standard protocol every browser's push service speaks (RFC 8030), the payload encrypted to the subscription (RFC 8291) and the sender identified by its VAPID key (RFC 8292).
Package plugin connects courier and a provider running as its own service over courier.plugin.v1.ProviderService.
Package plugin connects courier and a provider running as its own service over courier.plugin.v1.ProviderService.
proto
Package provider is what a courier provider implements: one way to deliver messages courier does not know.
Package provider is what a courier provider implements: one way to deliver messages courier does not know.

Jump to

Keyboard shortcuts

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