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.
TWO IDENTITIES, ONE EDGE Telegram identity channel proof actor = telegram_<id> Connection Hub edge explicit · carries grants the app hosts the link, the Hub owns it KDCube platform identity browser sign-in a1b2c3d4-… THE EDGE CARRIES SELECTED GRANTS identity:family economics:platform-user kdcube:role:* (optional) authority · economics · identity-family projected through the edge The actor stays telegram_<id> — platform projection flows only through selected grants. An edge with no grants proves linkage only — no identity-family reads, no economics, no platform authority.
The actor stays telegram_<id>. Platform authority, economics, and identity-family are projected only through the grants selected on the edge — no grants means linkage only.

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
MINI APP CONNECT FLOW — CREATING THE EDGE 1 Open Companion Telegram · initData ready 2 Embed Hub widget authContext headers 3 widget status / start Connection Hub ops 4 browser claim page sign in · confirm · grants 5 Hub stores edge linkage + selected grants 6 edge.changed Data Bus event 7 Mini App unlocks ✓ Memories · Chats The widget only carries authContext headers — the request authenticator validates the Telegram proof. Connection Hub owns the edge; the Mini App just reacts to edge.changed and unlocks features.
The widget only carries authContext headers; the request authenticator validates the Telegram proof, and Connection Hub owns the edge. The Mini App just reacts to edge.changed.

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
KDCube Journal · 30.06.2026