← Back to architecture

Telegram

chat integration · knowledge in the messenger

Company knowledge — right in your Telegram 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.

Telegram is one more thin window into the same conversation: Query Engine does the thinking and searching under the employee's identity, Telegram merely carries the message in and the answer out. Slack's twin — differing in one thing: the conversation is cut not by a thread, but by the /new command. One conversation model across all surfaces →

01

Connection

Admin registers a bot with @BotFather once — the bot token and webhook secret land in telegram_settings, like SMTP in Email. The screen is platform settings, a single toggle turns the bot on.

Achilles generates the webhook secret itself at setWebhook — the admin doesn't enter it, unlike the signing secret in Slack.

02

Identity

The sender is resolved to an account via identity_mapping (source='telegram') — the sign-in identity. Telegram doesn't expose an 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.

Public bot: 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 DM has no threads: the /new command opens a new conversation, all other messages continue the current one. /new is an adapter command; it never enters the conversation history. Context is truncated by tokens — the engine's shared mechanism.

The conversation lives in conversations (key chat_id in meta), the adapter holds the active one via a pointer in Redis. Groups · @mention · topics — v2.

04

Delivery

The webhook receives the update, acknowledges immediately, and processing goes to the interactive queue, the answer comes back as a message. It survives restarts and retries.

A repeated update is dropped by update_id; the public endpoint is rate-limited.

What it looks like

The answer is scoped to Maxim's permissions: anything he can't see won't appear in it. The next message continues the conversation, /new 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='telegram' → 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 chat_id
telegram:active-conv: Cache & Workers active-conversation pointer chat_id → conversation · /new resets it
dedup:telegram-update: Cache & Workers idempotency for repeated updates
telegram_settings bot connection · shared core singleton · CHECK (id = 1)
id BigInteger PKCHECK always 1 · singleton
bot_token_enc Text NULL bot token from BotFather (12345:ABC…), AES-256-GCM ciphertext
webhook_secret_enc Text NULL webhook secret (X-Telegram-Bot-Api-Secret-Token header) · Achilles generates it itself · ciphertext
bot_username Text NULL bot's @handle for the UI · from getMe
enabled Boolean NOT NULLDEFAULT master switch · DEFAULT false · off → the bot stays silent
last_test_ok Boolean NULL last delivery verdict: token live and webhook registered — not just a valid token
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 bot_token_enc · webhook_secret_enc filled in. Shares its core with slack_settings; differing in one thing — we generate the webhook secret rather than the provider issuing it.

Honest enable. Turning the bot on is atomic: Achilles calls setWebhook and the switch stays on only if Telegram accepts it — a refusal (revoked token) or an unreachable address rolls enabled back and answers with the reason, never a green switch with no delivery behind it. Telegram delivers only to a public HTTPS address, so a localhost/private PUBLIC_BASE_URL is rejected up front. "Test connection" reads the same truth — getMe plus getWebhookInfo — so a live token whose webhook isn't registered reads as not connected.

Checks: webhook secretidentity resolutionupdate_id dedup/new = new conversationACL pass-throughunlinked usercode guessing limit

API

The webhook is anonymous by design: the lock is the secret token in the X-Telegram-Bot-Api-Secret-Token header — which Achilles generated at setWebhook — and the rate-limit window is fail-closed.