Connecting A Telegram Channel To KDCube Through Connection Hub
Field notes on the Telegram channel integration for KDCube Companion: a Telegram webhook for messages, a Telegram
Mini App for UI, and Connection Hub linking the Telegram actor to a KDCube platform identity through an
explicit edge. The Telegram actor stays telegram_<id>; platform authority,
economics, and identity-family are projected only through selected grants on that edge.
This journal entry records the practical shape of the Telegram channel integration we now use for KDCube Companion: Telegram webhook for messages, Telegram Mini App for UI, and Connection Hub for linking the Telegram actor to a KDCube platform identity before platform-backed features are unlocked.
The important design decision is simple:
Telegram proves Telegram identity.
KDCube browser sign-in proves platform identity.
Connection Hub connects them with an explicit edge.
The Telegram actor remains telegram_<id>.
Platform identity-family, authority, and economics are projected only through
selected grants on that edge.
Telegram is not a platform role provider by itself. A verified Telegram user who is not connected to KDCube is an external actor. They should see the Connect flow, not Memories/Chats, and they should not receive platform registered-user quota.
Runtime shape
The message path resolves the Telegram actor, then asks Connection Hub whether an edge exists:
Telegram Bot API webhook
POST /public/telegram_webhook?integration_id=telegram.kdcube_ref
X-Telegram-Bot-Api-Secret-Token: <webhook secret>
|
v
bundle public Telegram endpoint
asks the configured Telegram integration/authenticator to verify the proof
resolves Telegram actor
|
v
Connection Hub request/auth edge resolver
|
+-- no edge
| reply: open KDCube Companion and use Connect
|
+-- edge exists
actor session = telegram_<id>
project platform authority/economics
run platform-backed workflow
The Mini App side uses the same authority model — it creates the edge the message path then relies on:
Telegram opens KDCube Companion
Telegram.WebApp.initData is available to the host
|
v
host embeds Connection Hub widget
CONFIG_RESPONSE.authContext.headers:
X-KDCube-Auth-Provider: telegram
X-KDCube-Auth-Integration-ID: telegram.kdcube_ref
X-Telegram-Init-Data: <initData>
|
v
Connection Hub widget status/start operations
|
v
Browser claim page
platform user signs in
confirms connection edge + selected platform-edge grants
|
v
Connection Hub stores edge
|
v
Data Bus event: connection_hub.edge.changed
|
v
Mini App updates and unlocks Memories/Chats
The iframe/widget does not validate Telegram itself. It receives an opaque authContext.headers map
from the host and promotes it on API calls. The actual Telegram proof validation belongs to the configured request
authenticator/integration, not to widget JavaScript.
The connection edge is not just "same person" metadata. It carries selected grants such as:
identity:family
allow read-side identity-family aggregation, for example Memories across
browser and Telegram runtime user ids
economics:platform-user
allow paid/economics checks to charge the linked platform identity while
preserving Telegram as actor provenance
kdcube:role:* or platform permission strings
optional platform authority grants, only when selected and still held by the
target platform identity
An edge with no grants can prove ownership/linkage, but it should not unlock identity-family reads, economics projection, or platform-authority behavior.
The two Telegram configuration surfaces
Telegram has two relevant app-entry surfaces:
Bot API chat menu button
can be changed by setChatMenuButton
can have default and per-chat overrides
BotFather Main Mini App URL
configured manually in BotFather
not changed by setChatMenuButton
This matters because a stale per-chat menu can keep opening an old widget alias even after the default menu
button is correct. We saw this with an obsolete versatile_webapp URL. The correct widget alias is
telegram_miniapp, and the canonical Mini App URL is:
https://<PUBLIC_HOST>/api/integrations/bundles/<TENANT>/<PROJECT>/<BUNDLE_ID>/public/widgets/telegram_miniapp
To clear a stale chat-specific menu:
curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setChatMenuButton" \
-H "Content-Type: application/json" \
-d "{\"chat_id\":${TELEGRAM_CHAT_ID},\"menu_button\":{\"type\":\"commands\"}}"
curl -sS "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getChatMenuButton?chat_id=${TELEGRAM_CHAT_ID}"
If Telegram still opens the old URL after that, check BotFather's Main Mini App URL.
Status is GET, mutations are POST
The Mini App needs to read whether this Telegram identity is already connected. That is a read operation and should be GET:
GET /api/integrations/bundles/<TENANT>/<PROJECT>/connection-hub@1-0/public/telegram_connection_edge_status
The other link operations mutate or mint state and remain POST:
POST telegram_connection_edge_start
POST telegram_connection_edge_complete
POST telegram_connection_edge_remove
POST federated_data_bus_claim
We also fixed the app-to-app bridge so peer bundle calls carry an explicit http_method. Without
that, a GET profile operation could accidentally make a nested Connection Hub status call with the wrong method and
get a 405.
What the user should see
Unlinked — the only useful UI is the Connect journey:
KDCube Companion
tabs: Connect
Connect tab:
Link Telegram to KDCube
explain:
Sign in to KDCube in the browser.
Confirm the link.
Return to Telegram.
Linked:
KDCube Companion
tabs: Memories · Chats · Connect
Connect tab:
Telegram account is connected
Telegram user id
Telegram nickname
KDCube platform user id
Unlink
If the actor is unlinked, platform-backed widgets should not show "user has no roles" or "user is required." They should not be shown yet.
Logs to look for
[connection-hub.request_authenticate] accepted
[telegram.connection] status provider=telegram provider_subject=... linked=... platform_user_id=...
[telegram.profile] actor_user_id=... telegram_user_id=... linked=... can_use_widget=...
[connection-hub.data_bus] claim issued ... actor_user_id=... linked=...
connection_hub.edge.changed
[connection-hub.identity_family_resolve]
[memory.identity_family] resolved
[run] init ... actor_user_id=... economics_user_id=...
Expected linked behavior:
actor_user_id = telegram_100200300
platform_user_id = a1b2c3d4-...
economics_user_id = a1b2c3d4-...
linked = true
can_use_widget = true
Expected unlinked behavior — authenticated externally, but not authorized as a platform user:
actor_user_id = telegram_100200300
platform_user_id = ""
linked = false
can_use_widget = false
Documentation on GitHub
The live docs behind this entry: