Schema and Data Model¶
All tables live in the pgrelay_notifier schema and are private: every privilege is revoked from PUBLIC and access goes through SECURITY DEFINER functions — see Security Model.
profiles¶
Delivery endpoint profiles (SMTP relays, Microsoft 365 tenants).
| Column | Type | Description |
|---|---|---|
profile_id |
bigint identity PK | Surrogate key referenced by notifications |
profile_name |
text | Unique identifier (case-insensitive; stored as created) |
transport |
text | smtp or m365; routes the Processor's Go handler |
profile |
jsonb | Transport-specific connection block returned verbatim by fetch(). Secret-bearing keys (password, client_secret) hold "_env:VAR_NAME" references, never literal values — the secret itself is never stored |
channel |
text | Default pg_relay channel for this profile's notifications |
send_from |
text | Default sender for notifications composed without p_sender. NULL: the profile JSON's own from/sender key applies |
reply_to |
text | Default Reply-To for notifications composed without p_reply_to. NULL: no Reply-To header by default |
max_attach_mb |
numeric | Cap on one notification's total attachment payload (MB of raw bytes; base64 adds ~37% on the wire) |
max_attach_number |
integer | Cap on attachments per notification |
debug |
boolean | Default debug-trace flag for the profile's notifications |
active |
boolean | Inactive profiles refuse new notifications; in-flight ones are unaffected |
notes |
text | Operator notes |
created_at / updated_at |
timestamptz | Audit timestamps (updated_at trigger-maintained) |
notifications¶
One row per message.
| Column | Type | Description |
|---|---|---|
notification_id |
bigint PK, DEFAULT pgrelay._next_id() |
The only value that travels through the pg_relay queue. Snowflake-generated — see Multi-Master and Notification IDs |
profile_id |
bigint FK → profiles | Delivery profile |
sender |
text | Per-notification sender override |
recipients |
text[] | To recipients (≥ 1) |
cc / bcc |
text[] | Copy recipients; bcc is envelope-only, never in headers |
reply_to |
text | Reply-To header |
subject |
text | Subject (Q-encoded automatically for non-ASCII) |
body_text / body_html |
text | Neither is required — a body-less compose() stores a single space in body_text as a placeholder, since the Processor's transports require at least one non-empty body part on the wire |
priority |
smallint 1–5 | Oracle UTL_MAIL-style priority; forward-compatible JSON key, not yet on the wire |
debug |
boolean | Per-notification debug override; NULL inherits the profile |
status |
text | draft, pending, sent, retry, failed, expired, invalid |
status_detail |
text | Failure detail from the last attempt |
provider_ref |
text | SMTP Message-ID or Graph request-id of the last attempt |
attempts |
integer | Highest attempt number reported (1-based) |
elapsed_ms |
integer | External-call milliseconds of the last attempt |
channel |
text | pg_relay channel used (set by dispatch()) |
dispatched_at / completed_at |
timestamptz | Queue entry time; terminal-status time |
created_at / created_by |
timestamptz / text | Compose time; the session_user that composed it (SECURITY DEFINER-safe provenance) |
attachments¶
| Column | Type | Description |
|---|---|---|
attachment_id |
bigint identity PK | Also the attachment order within the message |
notification_id |
bigint FK, ON DELETE CASCADE |
Owning notification |
filename |
text | Filename presented to recipients |
content_type |
text | MIME type; default application/octet-stream |
content |
bytea | Raw bytes; base64-encoded (no line wraps) at fetch time |
inline |
boolean | Oracle att_inline equivalent; forward-compatible key, not yet on the wire |
created_at |
timestamptz | When added |
status_history¶
One row per delivery attempt, written by the Processor via set_status(). Deliberately has no foreign key: the interface contract requires set_status() to tolerate unknown ids without raising, so unknown ids are logged here rather than rejected.
| Column | Type | Description |
|---|---|---|
history_id |
bigint identity PK | |
notification_id |
bigint | The attempt's notification (not FK-enforced) |
attempt |
integer | 1-based attempt number (retry_count + 1) |
status |
text | sent, retry, failed, expired, invalid (unconstrained — this is a log) |
detail |
text | Failure detail; may quote the provider's response |
provider_ref |
text | SMTP Message-ID / Graph request-id |
elapsed_ms |
integer | External-call milliseconds (0 when no send was attempted) |
recorded_at |
timestamptz | When recorded |
debug_trace¶
Processor step traces for debug-flagged notifications, written via debug_log() on an autocommit connection outside the delivery transaction — rows appear live during a send and survive rolled-back attempts. No foreign key (debug_log() must never raise).
| Column | Type | Description |
|---|---|---|
trace_id |
bigint identity PK | |
notification_id |
bigint | Traced notification |
step |
text | claimed, fetched, sent, send_failed, committed |
detail |
text | Step detail (queue id, transport, provider_ref, failure class…) |
logged_at |
timestamptz | clock_timestamp() — real time, not transaction time |
profile_defaults¶
Per-transport seed values copied into each new profile by create_profile(). Seeded at install with smtp: 10 MB / 999 and m365: 3 MB / 999.
| Column | Type | Description |
|---|---|---|
transport |
text PK | Transport the seed applies to |
max_attach_mb |
numeric | Default total-attachment-payload cap for new profiles |
max_attach_number |
integer | Default attachment-count cap for new profiles |
updated_at |
timestamptz | Maintained by update_profile_defaults() |
Deliberately not an extension member. A table whose rows the install seeds and the DBA edits can't round-trip pg_dump/restore as ordinary extension configuration — re-seeded rows would collide with dumped ones. Detaching it means DBA-edited seeds survive DROP EXTENSION, reinstall, and dump/restore, at the cost that \dx+ pg_relay_notifier won't list it and uninstalling fully means dropping the schema.