Mattermost¶
Mattermost is a self-hosted team-messaging platform — Slack's shape, but running on your own server. That makes it the answer to a question none of the other providers in this book can answer: how do notifications keep reaching people when there is no internet connection at all? A Mattermost server on your own network keeps the entire delivery path — PostgreSQL, the Processor, the chat server, and the people reading it — inside the LAN. Nothing leaves the building.
pg_relay reaches it through the webhook transport, and because Mattermost deliberately clones Slack's incoming-webhook payload shape, a working Slack setup translates almost line for line.
There are two ways in; both are worked below:
- an incoming webhook — the simplest possible integration, no token to manage;
- the REST API with a bot account — the closer mirror of the Slack
chat.postMessagerecipe, with a per-post provider reference.
Route 1: incoming webhook¶
One-time Mattermost setup¶
- A System Admin enables the feature once: System Console → Integrations → Integration Management → Enable Incoming Webhooks: true.
- Any user (or the admin) creates the hook: main menu → Integrations → Incoming Webhooks → Add Incoming Webhook, pick the default channel it posts to, and save.
- Mattermost shows the hook's URL:
https://mattermost.internal.example/hooks/<generated-key>.
That generated key is the only credential — anyone who can reach the server and knows the URL can post through the hook. Treat the whole URL as a secret and keep it out of the database with an _env: reference. This is a good moment to remember that _env: resolution is not limited to fields named like secrets — it walks every string in the profile, so the url field itself can be one:
Profile and message¶
// profile
{
"url": "_env:MATTERMOST_WEBHOOK_URL",
"auth": {"style": "bearer_header", "secret": "unused"},
"timeout_seconds": 30
}
// message — Slack-compatible incoming-webhook shape, sent verbatim
{
"text": "disk alert: volume is filling"
}
Why an auth block a webhook doesn't need?
An incoming webhook authenticates by URL alone and ignores the Authorization header entirely. But the webhook transport has no "no authentication" option — a missing auth.style is a permanent failure before any request is sent. So the profile carries a bearer_header block with a placeholder value: pg_relay sends a harmless Authorization: Bearer unused header, and Mattermost pays it no attention.
The message may also carry channel (a channel name, overriding the hook's default — e.g. "town-square"), and username / icon_url / icon_emoji overrides, each of which only takes effect if the System Console permits integrations to override those things.
Interpreting the response¶
Here Mattermost is better-behaved than Slack: it uses real HTTP status codes instead of answering 200 for everything. Have classify_webhook_response treat:
200assent. The body is the literal textok— there is no message id, so returnNULLas the provider reference.429, any5xx, andhttp_status = 0(no response) asretry— a429appears only if the server's API rate limiting is enabled, and self-heals through the retry chain.- any other
4xxasfailed— a malformed payload, or a hook key that no longer exists (someone deleted the integration). The JSON error body'smessagefield makes good detail text.
Route 2: REST API with a bot account¶
If you want a per-post provider reference and a credential you can revoke without changing a URL, use the API — this is the recipe that mirrors Slack's most closely.
One-time Mattermost setup¶
- System Console → Integrations → Bot Accounts → Enable Bot Account Creation: true.
- Main menu → Integrations → Bot Accounts → Add Bot Account — name it (e.g.
pg-relay), create it, and copy the access token Mattermost generates. Like Slack'sxoxb-token, it can be revoked and regenerated from the same page. - Add the bot to the team, and to the channel notifications should land in — a bot outside the channel cannot post to it.
- Note the channel's ID (channel name → View Info — the 26-character string), which the API requires instead of the channel's name.
Profile and message¶
// profile
{
"url": "https://mattermost.internal.example/api/v4/posts",
"auth": {"style": "bearer_header", "secret": "_env:MATTERMOST_BOT_TOKEN"},
"timeout_seconds": 30
}
// message — Mattermost's own create-post shape, sent verbatim
{
"channel_id": "8soyabwthjnf9qibfztje5a36h",
"message": "disk alert: volume is filling"
}
Note the field names differ from both Slack and the incoming webhook: the channel is channel_id, and the text is message, not text.
Interpreting the response¶
201(Created) assent— the body is the created post as JSON, and itsidfield is the natural provider reference.429, any5xx, andhttp_status = 0asretry.- any other
4xxasfailed—401is a bad or revoked token,403typically means the bot lacks permission or channel membership,400a malformed body or channel id. The JSON error body'smessagefield names the cause.
Formatting: Markdown, not mrkdwn¶
The payload shape is Slack-compatible; the text inside it is not. Mattermost renders standard Markdown — **bold**, [link](url), real # headings, tables — rather than Slack's mrkdwn dialect, and it has no Block Kit: the structured-layout equivalent is Slack-style message attachments (colour bar, fields, title), which both routes accept. If pg_relay_notifier composes for both targets, the two need separate rendering, not a shared string — *bold* means bold in Slack and italics in Markdown.
The air-gap dividend¶
Everything in this chapter works with the WAN cable unplugged, which is the point — but it cuts both ways. On an isolated network your Mattermost server's TLS certificate won't come from a public CA; the Processor's HTTP client trusts the host's system trust store, so install your internal CA certificate on the Processor host (or reach the server by plain http:// inside a network you already trust — the webhook transport does not require TLS). And if the Mattermost server itself goes down, http_status = 0 classification means every notification simply retries until it comes back — the queue in PostgreSQL is the store-and-forward buffer, which is rather the whole idea of pg_relay.
Continue to Microsoft Teams — the same webhook pattern again, pointed at a Power Automate workflow and a third message dialect: Adaptive Cards.