Skip to content

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.postMessage recipe, with a per-post provider reference.

Route 1: incoming webhook

One-time Mattermost setup

  1. A System Admin enables the feature once: System Console → Integrations → Integration Management → Enable Incoming Webhooks: true.
  2. 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.
  3. 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:

export MATTERMOST_WEBHOOK_URL=https://mattermost.internal.example/hooks/xxx-generated-key

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:

  • 200 as sent. The body is the literal text ok — there is no message id, so return NULL as the provider reference.
  • 429, any 5xx, and http_status = 0 (no response) as retry — a 429 appears only if the server's API rate limiting is enabled, and self-heals through the retry chain.
  • any other 4xx as failed — a malformed payload, or a hook key that no longer exists (someone deleted the integration). The JSON error body's message field 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

  1. System Console → Integrations → Bot Accounts → Enable Bot Account Creation: true.
  2. 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's xoxb- token, it can be revoked and regenerated from the same page.
  3. Add the bot to the team, and to the channel notifications should land in — a bot outside the channel cannot post to it.
  4. Note the channel's ID (channel name → View Info — the 26-character string), which the API requires instead of the channel's name.
export MATTERMOST_BOT_TOKEN=...

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) as sent — the body is the created post as JSON, and its id field is the natural provider reference.
  • 429, any 5xx, and http_status = 0 as retry.
  • any other 4xx as failed401 is a bad or revoked token, 403 typically means the bot lacks permission or channel membership, 400 a malformed body or channel id. The JSON error body's message field 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.