Skip to content

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:

SELECT pgrelay.register('welcome_email', '', p_action_type := 'notify');

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_retries and 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.