Core Concepts¶
Profiles¶
A profile is a named delivery endpoint — the connection details for one SMTP relay or one Microsoft 365 tenant, plus a few per-endpoint defaults (which channel it dispatches through, attachment limits, a default sender/reply-to). You name a profile every time you compose or send; your DBA creates and owns them (see the DBA Guide).
A database can have as many profiles as you need — a transactional-mail SMTP relay and a marketing M365 mailbox, for instance — and a single send picks exactly one.
Channels¶
A channel is the pg_relay queue lane a profile's messages travel through. Most of the time you never think about it: every profile has a default channel, and send()/dispatch() use it automatically. Channels matter when your DBA wants to control how a group of messages is dispatched — for example, serializing everything through one rate-limited relay with concurrency_mode = 'channel'.
The notification lifecycle¶
Every message you send is a notification — a row your DBA can inspect with get_status(). It moves through a small set of states:
draft ──dispatch()──▶ pending ──┬──▶ sent (terminal)
├──▶ retry ──▶ (loops back to pending)
├──▶ failed (terminal)
├──▶ expired (terminal)
└──▶ invalid (terminal)
draft— composed (compose()) but not yet enqueued. Invisible to the Processor; you can still add attachments.pending— enqueued, waiting for the pg_relay Processor to pick it up (usually under a second).sent— delivered.provider_refholds the SMTP Message-ID or Microsoft Graph request-id — quote it if you ever need to follow up with the provider.retry— a transient failure (timeout, connection refused, a 4xx/429/5xx from the provider); another attempt is already scheduled.failed— a permanent failure (bad address, rejected credentials) or retries exhausted.status_detailsays why.expired— you setp_expire_atand it passed before delivery.invalid— the channel was disabled or unregistered at dispatch time.
A notification that keeps failing transiently on a 2-retry channel shows retry → retry → failed; one that recovers shows retry → sent. See Checking Delivery Status for how to query this.
Two ways to send¶
One call (send()) — compose and dispatch together. This is the everyday path for a message with no attachments.
Two phases (compose() then attach()/attach_text() then dispatch()) — build a draft, add attachments, dispatch when ready. Required whenever a message carries an attachment; also useful if you want to build a message across several statements before committing to sending it.
Either way, the message commits with your transaction. Roll back, and nothing is ever queued, let alone sent.
At-least-once delivery¶
The pg_relay Processor claims a notification, fetches its content, sends it, and records the outcome — all inside one database transaction on the Processor's side. If the Processor crashes mid-send, that transaction rolls back and the notification is offered again on a later poll. The only edge case is a crash landing in the narrow window after the mail provider accepted the message but before the Processor's transaction commits — there, a duplicate is possible. A notification is never silently lost.
Priority and debug¶
Priority (p_priority, 1 highest – 5 lowest) is stored and carried in the message forward-compatibly; the current pg_relay Processor doesn't yet put it on the wire as an X-Priority header, but a future release can without any change on your side.
Debug (p_debug => true on a send, or a profile-wide default your DBA sets) turns on a live, step-by-step trace of one delivery attempt — see Debug Tracing.