← Back to architecture

Mattermost

chat integration · knowledge in the messenger

Company knowledge — right in your Mattermost chat

An employee messages the bot in a DM — Query Engine answers under their own permissions, with links to sources. Everything the web chat can do, without leaving the messenger.

Mattermost is one more thin window into the same conversation: Query Engine does the thinking and searching under the employee's identity, Mattermost merely carries the message in and the answer out. Slack's twin on a self-hosted server — the conversation is cut by the thread, and the transport is a dial-out WebSocket instead of a webhook. One conversation model across all surfaces →

01

Connection

Admin creates a bot account in Mattermost's System Console (Integrations → Bot Accounts) once — the server URL and bot token land in mattermost_settings, like SMTP in Email. The screen is platform settings, a single toggle turns the bot on. Enabling live-probes the token (GET /api/v4/users/me) and stamps bot_user_id — how the listener tells the bot's own posts apart.

Unlike Telegram there's no public-HTTPS requirement and no webhook secret at all — the listener dials out, nothing is exposed. A private-LAN server address is expressly fine (deliberately no SSRF guard: Telegram's guard protected the inverse direction, cloud → us).

02

Identity

The sender is resolved to an account via identity_mapping (source='mattermost') — the sign-in identity. A posted event carries no trustworthy email, so there's no auto-match: only a link code — whoever signed in on the web gets a code and returns it to the bot. Only an admin creates the account (by invitation) — knowing the bot grants no access.

An unlinked user gets a neutral refusal, code guessing is rate-limited. From there on it's only user_id → role → ACL.

03

Conversation

A Mattermost DM has threads, so the boundary is Slack's rule: a thread is a conversation. A reply in a thread continues it, a new root message starts a fresh one — no /new command, no active-conversation pointer. Context is truncated by tokens — the engine's shared mechanism.

The conversation lives in conversations (key channel_id + root_id in meta; no nested threads — a reply carries the true root's id). Groups · @mention · team channels — v2.

04

Delivery

Mattermost has no HTTP push for DMs, so nothing dials in: a singleton WebSocket listener in the scheduler process (the only 1-replica service) dials out to {base_url}/api/v4/websocket, authenticates with the bot token, and listens for posted events. It only transports — filters structural DMs (not the bot's own post, no system subtype, not another bot), processing goes to the interactive queue, the answer returns over REST as a post in the thread. It survives restarts and retries.

A replayed event is dropped by job id = post id; the per-channel window is rate-limited fail-closed. Listener health is a TTL'd Redis key the admin card shows as a quiet "listening / not connected" line; a settings change needs no signal channel — the listener re-reads them every 30 s and hangs up when its config changes.

What it looks like

The answer is scoped to Maxim's permissions: anything he can't see won't appear in it. A reply in the thread continues the conversation, a new root message opens a fresh one.

First contact — linking the account with a code:

The bot replies the same way to everyone — known user or stranger — never revealing that an instance exists. The code is visible only in the signed-in browser and comes back through the bot — a forwarded link can't hijack the account. Linking screen →

What's in the database

identity_mapping Auth · edited + source='mattermost' → sign-in identity bridge
link_tokens Auth · shared one-time link code (signed in on the web → returned to the bot)
conversations.meta Query Engine conversation key channel_id + root_id
cache:mattermost:listener Cache & Workers listener health · TTL'd (90 s) — an expired key honestly reads "not running"
mattermost_settings bot connection · shared core singleton · CHECK (id = 1)
id BigInteger PKCHECK always 1 · singleton
base_url Text NULL server address (https://mattermost.company.com) · any API-v4 installation, private LAN fine
bot_token_enc Text NULL bot access token from the System Console, AES-256-GCM ciphertext
bot_user_id Text NULL from /users/me · stamped only by a successful probe · lets the listener skip the bot's own posts
bot_username Text NULL bot's @handle for the UI · from /users/me
enabled Boolean NOT NULLDEFAULT master switch · DEFAULT false · off → the bot stays silent
last_test_ok Boolean NULL last probe verdict: the token answered — delivery health is the listener's connected flag, shown separately
last_test_at DateTime(tz) NULL when it was tested · feeds the status chip on the screen
created_at DateTime(tz) DEFAULT server_default=now()
updated_at DateTime(tz) DEFAULT server_default=now() · trigger set_updated_at()

is_available() — a model property, not a column: enabled and base_url · bot_token_enc · bot_user_id filled in. bot_user_id plays the "honestly wired" role webhook_secret_enc plays for Telegram — present only after the token really answered.

Honest enable. Turning the bot on is atomic: Achilles live-probes the token with GET /api/v4/users/me and the switch stays on only if the server answers — a refusal (revoked token, unreachable address) rolls enabled back and answers with the reason, never a green switch with no bot behind it. The probe also stamps bot_user_id and bot_username. "Test connection" reads the same truth — the probe vouches for token + server; delivery is vouched for by the listener's connected flag.

Checks: listener filter (own posts · bots · system)identity resolutionpost-id dedupthread = conversationACL pass-throughunlinked usercode guessing limithonest enable

API

There is no webhook endpoint: the only inbound path is the dial-out WebSocket, so no anonymous route exists at all.