ADR-0006 — Business Bot, single bot with two roles
**Status.** Accepted.
Status. Accepted. Date. 2026-05-18.
Context
Telegram Business lets a Premium user connect a bot to their account. The bot then receives business_connection, business_message, edited_business_message and deleted_business_messages updates for the chats the owner controls — exactly the data we already collect from userbots, but with first-party permission.
The owner chose to keep a single bot (@HeliosBot) carrying both roles: the existing tracking experience and the new Business surface. Two interaction modes inside one token, sharing the same database and operator panel.
Decision
Token & registration
- One bot, one token. Enable Business Mode in BotFather (
/setbusiness). - No second bot, no second webhook. The same FastAPI route handles both
Update.business_*and the existingUpdate.message/....
Code partitioning
apps/bot/
├── handlers/
│ ├── start.py
│ ├── common/, user/, billing/, owner/, admin/, team/
│ └── business/ ← new subpackage
│ ├── connection.py — handles business_connection updates
│ ├── messages.py — business_message
│ ├── edits.py — edited_business_message
│ ├── deletes.py — deleted_business_messages
│ └── reactions.py — message_reaction in business chats
core/
├── domain/business.py — BusinessConnection, BusinessRights, ChatBinding
infrastructure/
├── db/models/business.py — business_connections, business_messages,
│ business_message_edits, business_chats
Business logic stays neatly partitioned; the rest of apps/bot/ does not import from handlers/business/ and vice-versa, and shared concepts go through core/.
Domain model
BusinessConnection:
id— provider idbusiness_owner_tg_user_iduser_chat_id— the deep-link target (/start bizChat<user_chat_id>)can_reply— write permissionis_enabledconnected_at,disconnected_at
BusinessRights — keep a typed snapshot of every right granted (the API uses the new BusinessBotRights object, but we map to a stable dataclass to insulate against schema drift).
We always treat the bot as read-only in business mode for v1 — even if can_reply is true. Future write capabilities (auto-reply, scheduled messages) ride on a feature flag.
What we capture
Per connected chat:
- Every
business_message→business_messageshypertable, JSONB raw payload + extracted fields. edited_business_message→business_message_editsappend-only, with computed diff (see ADR re: diff renderer).deleted_business_messages→ soft-delete inbusiness_messageswithdeleted_at, retained until retention TTL elapses for the owner.message_reaction(Bot API now supports this for business chats) →business_reactions.- Stickers (regular, animated TGS, video WebM), animated emoji, premium emoji — captured as media records, deduped by
file_unique_id, stored at original format plus an optimised derivative per ADR-0009.
Tables (sketch)
business_connections (id, owner_tg_user_id, user_chat_id, can_reply,
is_enabled, rights JSONB, connected_at, disconnected_at);
business_chats (id, connection_id, chat_id, title, type, joined_at,
last_seen_at);
business_messages (hypertable; connection_id, chat_id, message_id, from_id,
date, raw JSONB, has_media, deleted_at, expires_at);
business_message_edits (append-only; connection_id, chat_id, message_id,
edited_at, diff JSONB);
business_reactions (connection_id, chat_id, message_id, user_id, emoji,
custom_emoji_id, added_at);
business_media (file_unique_id PRIMARY KEY, file_id_latest,
content_type, original_size, stored_size,
storage_key, sticker_set, is_premium_emoji);
business_* tables are scoped by connection_id; the existing RLS middleware sets app.client_id and additionally app.business_connection_id for the business subsystem.
Detect granted permissions
The business_connection update carries the rights object. On each update we upsert the business_connections row with the latest can_reply and rights. The Mini App "Business" page shows the current set as toggles, with a tooltip describing which features each unlocks.
If a previously-granted right disappears (owner revokes), we deactivate the corresponding features automatically and emit business.right_revoked.
What the user sees
A new top-level menu entry "Business" appears for any client that has at least one business_connections row. It exposes:
- Connected chats with message counts, edit/delete tallies, last activity.
- Searchable history (Meilisearch wrapped, scoped to the business connection).
- Edit / delete view per message — minimalist diff (added/removed segments highlighted).
- Per-chat analytics (volumes by hour, by sender, by sticker pack).
- Export ZIP archive of the business chat (HTML + JSON + media).
Deep-link entry
/start bizChat<user_chat_id> — handled by apps/bot/handlers/start.py (still the only command). It routes to the Business landing screen for that specific managed chat.
Pricing
Business surface is Premium+ by default — a new feature flag in the matrix Feature.BUSINESS_BOT gates everything. Owner can grant Pro-tier access per-client via the same overrides system used elsewhere.
Restrictions Telegram imposes
- Secret chats are invisible to bots — we cannot capture those.
- Outbound (if we ever enable it) is limited to the 24-hour window since the chat's last activity.
- The bot must be the active connected bot at the moment of the update; concurrent bots are not allowed by Telegram.
We surface these constraints honestly in the Mini App.
Consequences
- One token, one webhook, one observability stack.
- Strict subpackage boundary (
apps/bot/business/imports nothing fromapps/bot/handlers/{user,owner,admin,team}). BusinessConnectionAdapterProtocol gives us a clean seam if we ever split into a second bot — that work is mostly a directory rename and a token swap.
Alternatives considered
- Two bots. Rejected for ux complexity and DevOps overhead.
- Treat business as just another track type. Rejected because the consent model and data ownership are different (the owner of the business account opts in to the bot).
See also
- ADR-0004 (resilience also covers business-update gaps when the connection drops)
- ADR-0009 (media compression — applies to business media too)
- ADR-0010 (analytics tiers — business analytics dashboards)
apps/bot/handlers/business/,core/domain/business.py