Architecture Overview¶
pg_relay_notifier is a pure SQL/PL/pgSQL PostgreSQL extension. It introduces no C code, no background workers, and no shared_preload_libraries entry.
The extension implements the notifier side of the pg_relay v1.1 interface contract (interface version 1). The split is strict and deliberate:
- pg_relay_notifier owns all notification data: recipients, subjects, bodies, attachments, connection profiles, delivery status, debug traces. It stores them in its own tables, in its own schema (
pgrelay_notifier), with whatever structure it chooses. - pg_relay owns the delivery mechanics: queueing, ordering, concurrency, retries, timeouts, the transport implementations (SMTP and Microsoft Graph, in Go), and the audit trail in
pgrelay.log.
The two meet only at four interface functions — see The Interface Contract. The pg_relay Processor never touches a pgrelay_notifier table; the notifier never touches queue internals. Because the interface is functions, pg_relay_notifier is free to restructure its own tables in any future 1.x release without a pg_relay release — only a change to the four function signatures themselves would require coordination.
How a notification flows¶
application / trigger
│ writes the notification into pg_relay_notifier's tables
│ (recipients, subject, body, profile reference, ...)
▼
pg_relay_notifier enqueues: SELECT pgrelay.notify('<channel>', '<pk>')
│ the payload is ONLY the primary key of the notification row
▼
pgrelay.queue ← durable row, committed with the producer's transaction
⋮ (up to ~1 second)
pg_relay Processor tick:
1. pgrelay.queue_probe() → work? reload requested? paused?
2. pgrelay.queue_pending_ids() → (id, action_type) list
3. action_type = 'notify' → IN ONE HELD TRANSACTION:
a. pgrelay._queue_claim(id) row locked until COMMIT
b. pgrelay_notifier.fetch(pk) full message + profile
c. deliver (SMTP / M365 Graph) under a hard timeout
d. pgrelay_notifier.set_status(pk, ...) attempt outcome
e. pgrelay._write_log(...) audit row
f. pgrelay._queue_mark_done(id) (+ retry row on transient failure)
g. COMMIT
▼
pgrelay.log — one audit row per attempt pg_relay_notifier — delivery status per attempt
The held transaction is the heart of the design: the claimed queue row stays row-locked for the whole delivery, so SKIP LOCKED hides it from every other Processor instance and concurrency_mode behaves exactly as it does for ordinary SQL channels. A crash mid-send rolls back to an unclaimed row that's re-offered on a later poll: delivery is at-least-once — a rare duplicate is possible if a crash lands between provider acceptance and COMMIT, but a notification is never silently lost.