How Notifications Work¶
Who needs this book
Anyone who wants pg_relay to send something out of the database — an email, a Slack message, an SMS, a webhook call, a ticket. It needs the companion extension pg_relay_notifier. If your actions are all SQL, you can skip this book.
pg_relay can deliver email and other outbound notifications directly from an event, using the same durable queue, retry chain, and audit log as every other channel. It does this in partnership with a companion extension, pg_relay_notifier, which is not part of pg_relay itself and must be installed separately.
The division of labour is simple:
- pg_relay_notifier stores what to send — the recipients, subject, bodies, attachments, provider credentials, and the transport to use.
- pg_relay's Processor does the sending — it fetches that record, delivers it, and reports the outcome back.
A channel that sends notifications is registered like any other, except with p_action_type := 'notify' instead of the default 'sql'. Because a 'notify' channel has no SQL action of its own, p_action is left empty:
The payload you pass to pgrelay.notify() for a 'notify' channel is not free-form text — it must be the primary key of a record already sitting in pg_relay_notifier's own tables, as a plain integer:
-- 42 is the id of a notification pg_relay_notifier already knows about:
SELECT pgrelay.notify('welcome_email', '42');
Anything else in that payload position — text, an unknown id, an empty string — resolves the event permanently as an error. It never turns into a delivery attempt.
The five transports¶
| Transport | Use this when |
|---|---|
smtp |
You want to send email directly over SMTP — your own mail server, a Gmail/Google Workspace mailbox, Exchange Online's SMTP AUTH OAuth2 replacement, or the Azure Communication Services SMTP relay. |
m365 |
You want to send email through Microsoft 365 / Exchange Online using the Graph API rather than SMTP. |
acs |
You want to send email through Azure Communication Services' own REST API rather than its SMTP relay. |
gmail |
You want to send email from a Gmail or Google Workspace mailbox through the Gmail REST API rather than SMTP. |
webhook |
You want to call an HTTP API pg_relay isn't natively aware of — SendGrid, Slack, Teams, an internal service, JSON or form-encoded — and let pg_relay_notifier decide what a success or failure response looks like. |
Each transport gets its own chapter in this book: SMTP, Microsoft 365, Azure Communication Services, Gmail, and Webhook.
Provider recipes¶
Microsoft and Google services have transports of their own, above. For other popular email, chat, SMS, alerting, and ticketing providers, this book carries ready-to-adapt recipe chapters — each names the transport that reaches the provider and gives working profile and message shapes:
| Provider | Reached through |
|---|---|
| Amazon SES | smtp |
| SendGrid | smtp or webhook |
| Postmark | smtp or webhook |
| Mailgun | smtp |
| SMTP2GO | smtp or webhook |
| Resend | smtp or webhook |
| Slack | webhook |
| Mattermost | webhook |
| Microsoft Teams | webhook |
| SMS API providers — Twilio, Telnyx, Vonage, and more | webhook |
| Incident alerting platforms — PagerDuty, JSM Operations, and more | webhook |
| Jira — creating issues, with the issue key returned | webhook |
| ServiceNow — creating incidents, with the incident number returned | webhook |
The Slack recipe pairs with Formatting Slack Messages — mrkdwn, Block Kit alert layouts (headers, severity colors, fields, buttons), and deep links into runbooks and guides.
Profile and message¶
Every notification pg_relay_notifier hands to the Processor is split into two JSON blocks:
profile— the connection: server, credentials, sender address, timeouts. This is fleet configuration, and it is where secret values live (see Keeping Secrets Out of the Database).message— the content: recipients, subject, bodies, attachments, or (for webhooks) the entire request body.
The Processor never logs message content, at any level, on any path. Only correlation identifiers — the channel name, the queue id, the audit log id, and (for a rejected SMTP recipient) a category-and-position label — ever reach its own log lines. What actually happened to a send is recorded in the database, in pgrelay.log and in pg_relay_notifier's own tables, never in the Processor's stdout.
Timeouts¶
Every send runs under a per-attempt timeout, taken from profile.timeout_seconds. If you don't set it, it defaults to 30 seconds. Whatever you set it to, it is capped at 120 seconds — no notification profile can wedge a Processor worker beyond two minutes, no matter what a slow or hung server on the other end does.
Transient versus permanent failure¶
When a send fails, the Processor classifies the failure as one of two kinds, and the difference changes what happens next:
- Transient — the kind of failure where trying again might work: a busy mail server replying with a temporary rejection, a provider's API throttling you, a network timeout, a dial failure. These go through pg_relay's ordinary retry chain — the same
max_retriesand backoff schedule described in Retry Policy — and the event is retried automatically up to the channel's configured limit. - Permanent — the kind of failure where trying again cannot possibly help: a mail server outright rejecting the message, a malformed profile, a missing secret, an unknown recipient the provider has permanently refused. These resolve immediately as
error, with no retry attempted — pg_relay does not burn retry attempts on a failure that will simply happen again.
Exactly what counts as transient or permanent is specific to each transport, and is covered in that transport's own chapter.
One held transaction per send¶
Unlike a plain SQL event, sending a notification takes more than one database statement — claim the row, fetch the details, do the send, record the result. All of that happens inside one transaction, held for the whole sequence, on a single Processor connection. This is what keeps a notification channel behaving exactly like any other pg_relay channel under concurrency: the claimed row stays locked for the entire external send, so no other Processor instance can pick up the same event mid-flight, and concurrency_mode (see Workers) applies identically. If the Processor crashes partway through a send, the transaction rolls back to an unclaimed, re-offerable row — the event is tried again, never silently lost. This is the same at-least-once delivery guarantee pg_relay gives every other event.
Continue to Keeping Secrets Out of the Database, or jump straight to the transport chapter you need.