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.
posted event · WebSocket (dial-out)
answer as a post in the thread
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 →
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).
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.
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.
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's the status of release 4.2 and who owns the ajax bug?
Release 4.2 is in regression testing, ship date planned for Friday. The ajax bug is owned by Dina Safina1.
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:
Hi! What's the status of release 4.2?
To continue, confirm your account: sign in at achilles.local/link and send me the code. After that I'll answer under your permissions.
K7P2-9XQ4
Done — linked to maxim@company.com. Ask away
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 →
source='mattermost' → sign-in identity bridge
channel_id + root_id
| 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
There is no webhook endpoint: the only inbound path is the dial-out WebSocket, so no anonymous route exists at all.
identity_mapping → user_id bridge, shared
link_tokens, link-code protection.
→ Auth / Security
/api/v1/admin/mattermost paths on the shared surface map.
→ HTTP API