Skip to Content
Bronit HotelSetting up the Telegram Mini App

Setting up the Telegram Mini App

⏸️ The Mini App is currently parked

It is switched off in every environment, on purpose, until we decide we want it. Nothing has been deleted — the whole feature is intact in the tree and this page is still the way to turn it on.

The switch is TELEGRAM_MINIAPP_NAME. Unset, as it is now, means: no launch buttons on notifications, no /app command, and the Mini App’s own pages answer 404. Set it to the BotFather short name and everything below applies again — there is no second flag and no code to restore.

Two things deliberately kept working while it is parked: /menu/<hotel>/tma (the guest menu inside Telegram — that is the ordinary public menu, and a guest standing in a room should not meet a 404), and every bot command that is not about the Mini App.

Everything BotFather and the environment need before the Mini App works. The code is already built (#185, #232) — nothing here requires a deploy, and none of it can be done from inside the app.

The failure mode to know about first: with TELEGRAM_MINIAPP_NAME unset, every launch button returns null and notifications go out with no way in. The app does not error, log a warning, or look broken. It goes quiet. Most of this page exists so that does not happen.


app.bronit.uz is gone. Every origin on this page used to name it; the record was deleted in the hostname split and the app is served from hotel.bronit.uz. /opt/bronit/.env on production carries the new value for both NEXT_PUBLIC_APP_URL and TELEGRAM_WEBHOOK_URL, and the webhooks were re-registered — a bot pointed at the old origin receives nothing, silently.

What you need before starting

A Telegram botCreated by @BotFather ; you need its token
A public HTTPS originhttps://hotel.bronit.uz in production — Telegram will not talk to plain HTTP or to localhost
Access to the server’s .envOr wherever the deployment reads environment variables

1. Environment variables

Six for the standard app, and only the first two are strictly required to send anything. Bronit POS adds three more — see 4b.

# The bot's token from BotFather. Without it every Telegram call no-ops — # deliberately, so a developer without one can run the whole app. TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # The bot that HOSTS the Mini App, if it is a different bot from the one above. # Bronit runs three Telegram surfaces (#438): a notification bot, a Mini App # bot, and a per-company bot for custom Mini Apps. Unset means "the same bot # does both", which is the configuration everywhere today -- so leaving this # out changes nothing. # # It matters because Telegram signs `initData` with the token of whichever bot # SERVED THE APP. Point the Mini App at a second bot without setting this and # every sign-in fails with "signature does not match". TELEGRAM_MINIAPP_BOT_TOKEN=234567:GHI-JKL... # Where the app lives. Used to build Mini App URLs and web_app buttons. NEXT_PUBLIC_APP_URL=https://hotel.bronit.uz # The Mini App's SHORT NAME as registered with BotFather in step 3. # NOT the bot username, and not the URL. See the warning below. TELEGRAM_MINIAPP_NAME=app # A secret Telegram sends back with every update, so the webhook can refuse # anything that did not come from Telegram. Any long random string. TELEGRAM_WEBHOOK_SECRET=<32+ random characters> # Optional: the default origin for `npm run telegram:webhook -- set`, # so the command can be run without an argument. TELEGRAM_WEBHOOK_URL=https://hotel.bronit.uz

The short name and the token belong together. platformMiniAppBot() returns null unless it has both, because a short name with no token builds links to an app that cannot authenticate whoever opens them, and a token with no short name cannot build a link at all. Half-configured is treated as not configured.

TELEGRAM_MINIAPP_NAME is the one that fails silently. miniAppLink() returns null when it is unset, and every caller treats null as “no button” rather than as an error — because a missing Mini App must never stop a booking notification going out. The result is a bot that sends messages nobody can act on. If buttons are missing, check this first.

Restart the app after changing any of these.


2. Register the webhook

The webhook is what delivers messages and button presses to the app. Registering it by hand with curl is how it kept coming undone, so there is a script:

npm run telegram:webhook -- set https://hotel.bronit.uz npm run telegram:webhook -- status # inspect what Telegram thinks npm run telegram:webhook -- delete # unregister

set takes the origin, not the full path — the route is always /api/telegram/webhook, and hardcoding it in the script removes one way to get it wrong.

Two things the script handles that hand-rolled curl historically did not:

  • allowed_updates includes callback_query. Omit it and every inline button in the group silently stops working — messages still arrive, presses do nothing.
  • The secret is re-sent. Rotating TELEGRAM_WEBHOOK_SECRET in .env without re-running set makes the route reject every update with 403, and nothing reaches the app.

Run status after any change. It shows the URL Telegram has, the pending update count, and the last error.


3. Create the Mini App in BotFather

In a chat with @BotFather :

  1. /newapp
  2. Choose your bot.
  3. Title — what users see, e.g. Bronit.
  4. Description — one line.
  5. Photo — 640×360.
  6. GIF — optional; send /empty to skip.
  7. Web App URL — https://bronit.uz/ru/secure/tma/dashboard
  8. Short name — this becomes the last part of t.me/<bot>/<shortname>.

The full path, not the bare origin. This step used to say https://hotel.bronit.uz, and that is the single instruction that makes the Mini App look broken: / is the website. On the apex it redirects to the marketing page and on app. it redirects to the dashboard’s sign-in form — so staff open the Mini App and are asked to log in by an app that deliberately has no login. Nothing errors; it just serves the wrong product.

It cannot self-correct, either. Telegram passes initData in the URL fragment (#tgWebAppData=…), which browsers never send to the server, so proxy.ts cannot notice that a request for / came from Telegram. The only place the entry point can be recorded is here.

Use the same origin as NEXT_PUBLIC_APP_URL — that is https://bronit.uz in production today, not app.. Check the box rather than trusting this line; see the note in miniApp.ts about comments that assert what an environment holds.

Put that short name in TELEGRAM_MINIAPP_NAME. If you chose app, the direct link is https://t.me/YourBot/app, and the variable is app.

The Menu Button

Separate from the Mini App, and easy to confuse with it:

  1. /mybots → your bot → Bot Settings → Menu Button → Configure
  2. Set the URL to https://bronit.uz/ru/secure/tma/dashboard

The menu button is the persistent button beside the message box. The Mini App is what deep links and inline buttons open. Both should point at the same URL — the full path above, for the reason in step 7. Pointing the menu button at the bare origin is the same bug wearing a different hat: one of the two ways in opens the app and the other opens the website, which reads as the Mini App randomly logging people out.


4. Which URL opens what

The app routes by the startapp parameter, so one Mini App serves every surface. There is no second app to register.

LinkOpens
t.me/<bot>/<app>Staff dashboard, after sign-in
t.me/<bot>/<app>?startapp=smmContent orders
t.me/<bot>/<app>?startapp=order_<id>The order that notification was about
t.me/<bot>/<app>?startapp=booking_<id>That booking’s calendar
t.me/<bot>/<app>?startapp=guest_<hotel>_<room>A guest’s room menu — this is what a QR code encodes

Telegram permits only A-Za-z0-9_- in startapp, up to 64 characters. The app refuses to build a link it cannot read back — a hotel slug containing an underscore returns no link rather than one that resolves to the wrong hotel.


4a. A custom-tier company’s own Mini App

A business on the custom version runs its own bot with its own app registered on it, so nothing their staff sees says Bronit. A superadmin attaches one from Superadmin → the company → Bot; no deploy and no code change.

  1. The client creates a bot with BotFather and hands over the token.
  2. Paste it into the Bot tab. Bronit runs getMe, encrypts the token and registers a webhook with a secret unique to that bot.
  3. In BotFather, run /newapp against their bot.
  4. Put the short name into the same tab and save.

The Web App URL is the step that silently breaks sign-in

/newapp asks for a URL. It must carry the company’s slug:

https://<your-origin>/ru/secure/tma/dashboard?c=<company-slug>

The Bot tab prints the exact string once a short name is saved — copy it from there rather than typing it.

Why. Telegram signs initData with the token of whichever bot served the app. A request arriving from the client’s bot is signed with the client’s token, and the server has to know that before it can check the signature — which means before anything in the request has been verified. The only thing that arrives that early is the URL we registered, so the slug rides on it.

Register the app without ?c= and it opens perfectly and nobody can sign in: every attempt returns “Could not verify Telegram sign-in”, because the signature is being checked against the platform bot’s token.

The slug is a hint and nothing more. It chooses which token verifies the request; it grants nothing. Naming another company gets that company’s token checked against a signature it did not produce, which is a 401 — the same answer an unauthenticated caller already had. That is why a public slug is safe here.

Leaving it empty

A company with a bot but no short name keeps the platform Mini App, opened from their own bot. That is every custom-bot company today, and clearing the field puts them back there — the fallback is by design, so this is reversible.

Branding

Three fields on the same Bot tab, saved separately from the short name:

FieldWhere it showsEmpty means
Namethe app’s own copy, wherever ours would say BronitBronit
Logoabove the greeting on the screen before sign-inno logo
Welcome messagereplaces our “you need to link an account” lineour line

The three numbered linking steps are not editable. They are instructions, and getting them wrong costs somebody their sign-in.

All three are applied only while the company is on its own bot. A business that moves back to the platform app stops showing them immediately, without anyone clearing a field — the gate is whether they have their own Mini App, not whether the boxes are blank.

No colour

Deliberately, and it is worth knowing why before anybody asks for one.

A Mini App does not own its palette. applyTelegramTheme maps the viewer’s own Telegram colours onto our tokens on every open — their background over --surface-bg, their button colour over --brand-500, their text colour over the greys — so a stored brand colour would be overwritten before the first frame is painted.

The only way to honour one is to stop adopting the viewer’s theme at all, which buys a brand colour at the price of ignoring their dark mode, on a phone, for staff who work evenings. Name, logo and words are the parts their staff actually read, and none of them fights the client.

What is not automated

BotFather is a conversation, not an API. Creating the bot and running /newapp are manual, and there is no Bot API call that reads a registered short name or URL back — so nothing on our side can verify either. Open the link once.


4b. Bronit POS, the restaurant bot

@bronituz_pos_bot is a second platform bot, not a company’s. One bot serves every restaurant, exactly as the standard app serves every hotel — there is no per-restaurant configuration and nothing for a superadmin to attach.

A company is on POS when its business type is restaurant (Company.businessType, set at signup and editable in Superadmin → the company). That single field decides it; there is no separate switch.

The variables

# The POS bot's token. This is the half that authenticates: Telegram signs # initData with the token of whichever bot served the app, so without it every # POS sign-in fails with "Could not verify Telegram sign-in". TELEGRAM_POS_BOT_TOKEN=345678:MNO-PQR... # The POS app's short name from /newapp. OPTIONAL, unlike TELEGRAM_MINIAPP_NAME: # the token alone is enough to verify a sign-in and to name the bot on screen, # and this is needed only to build t.me/<bot>/<app> links. TELEGRAM_POS_MINIAPP_NAME=pos # Its webhook secret. Falls back to TELEGRAM_WEBHOOK_SECRET. TELEGRAM_POS_WEBHOOK_SECRET=<32+ random characters>

Registering it

npm run telegram:webhook -- set https://hotel.bronit.uz --pos npm run telegram:webhook -- status --pos

--pos switches the token, the path (/api/telegram/webhook/pos) and the menu button together. All three move as one on purpose: pointing the POS bot at the shared /api/telegram/webhook half-works, which is worse than failing. The update is handled, but that route establishes no bot identity, so every reply goes out as @Bronit_Uz_Bot — the waiter waiting in the POS chat sees nothing, and is then messaged out of the blue by a bot they never opened.

Unlike the shared route, /api/telegram/webhook/pos refuses updates when no secret is configured. The shared one predates the secret and tolerates its absence; nothing has ever pointed at the POS path, so there is no deployment to be lenient towards. A POS bot registered with no secret is a silent bot.

The Web App URL

https://<your-origin>/ru/secure/tma/waiter?app=pos

The ?app=pos is what 4a’s ?c=<slug> is for a custom company: the only thing that reaches the page before anybody is signed in is the URL registered in BotFather, and the server needs to know which token to check the signature against before it has verified anything. Register it without ?app=pos and the app opens perfectly and every waiter is told their sign-in could not be verified.

What a restaurant sees in Settings → Telegram

The card at the top of that page — “your Telegram is not connected” — names POS, and its one-time link opens POS. Notifications are unaffected: they go on coming from the shared bot, which is what the Bot and group tab below is about. Two bots on one screen because there are two questions, and only the first one is about the bot a person has open all day.

A restaurant on the custom tier is an exception to the exception: their own bot wins over POS, because it is the only bot their staff see.

Half-registered is a supported state

A POS bot with a token but no /newapp verifies sign-ins and is named on the settings screen, and simply does not take over notification links — restaurants keep the standard app’s t.me buttons until the short name is set. That is why posMiniAppBot() resolves on the token alone while miniAppBotFor skips a POS bot with no short name.


5. Linking an account, once per person

The Mini App has no sign-in form. Staff who open it before their Telegram account is linked are shown three steps and sent back to the chat.

Which bot is not always the shared one — a restaurant’s staff are sent to Bronit POS and a custom-tier company’s to their own bot. The warning card names the right handle and its one-time link opens it, so read it from the screen rather than assuming @Bronit_Uz_Bot. The account itself is linked by Telegram user id, so any of these bots would in fact do it; naming the wrong one just sends somebody to a chat they have no reason to open.

  1. Send the bot /start in a private chat. It replies with the person’s Telegram ID and a one-time code — both as tap-to-copy chips, both live for ten minutes.
  2. Open Bronit in a browser, signed in as the person being linked, and go to Settings → Telegram. Until this is done the page opens with a red warning carrying these same three steps.
  3. Paste either value into the field and press Link. From then on, opening the Mini App signs them in silently, and every bot command that acts on somebody’s behalf knows who they are.

Both halves are still required and neither is sufficient alone — Telegram delivering the value proves which Telegram account, and the dashboard session that spends it proves which Bronit login. It replaced a password form because that asked for a second credential on a phone keyboard when the first had already been proved by Telegram, and it was where people stopped.

Why the ID is safe to accept. A Telegram ID is not a secret — anyone in a group with you can read it. What the server accepts is not the number but a pending /start: a record that exists only because that account messaged the bot, and that expires ten minutes later. Pasting an ID you happened to know does nothing unless its owner pressed Start moments ago. The one-time code is the stronger of the two and is on the same screen for anyone who wants it.

The one-time link in the other direction — the dashboard mints t.me/<bot>?start=link_… and Telegram delivers it — is still there, under “No access to the bot right now?” on the same card. It is the fallback for somebody who cannot reach the bot from the machine they are on.

Guests are never asked for anything.

If someone needs to switch accounts, they sign out in the Mini App (which unlinks) and link again with a fresh code.


5a. What the bottom bar carries

The Mini App draws three tabs and a ☰ button. Which three is a choice, not a constant, because a hotel running room service and an agency running content orders want different ones.

Nine screens can go on it, grouped the way the sidebar groups them:

GroupScreens
GeneralToday, Clients, Statistics, History
Bronit ServiceCalendar
Bronit MenuOrders
Bronit SMMContent
AccountProfile, Settings

Profile and Settings joined the list once the ☰ started carrying whatever the bar left out. They used to be pinned rows in that menu, which is exactly why they could not be offered here — putting one on the bar would have drawn it twice.

Two people can set it, and the more specific one wins:

  • The owner, in Settings → Telegram → Mini App, for the whole company.

  • A hotel’s manager, in the Mini App’s own ☰ → Settings, for their hotel only. Their choice replaces the company’s for everyone at that property, and Follow the company hands the decision back.

    It is there rather than in Settings because the proxy keeps /settings out of the hotel-admin area entirely — Settings is where a business is configured.

A company that has never opened either screen gets a bar derived from what it owns: Today and Calendar, plus Orders if it has Bronit Menu, or Content if it has Bronit SMM. Nothing is written to record agreement with that.

Exactly three must be chosen, and a screen left off the bar is still reachable from the ☰ — this arranges the menu, it does not restrict anybody. Roles and modules decide what a person may open, exactly as before. A tab whose module lapses simply disappears from the bar; the slot is not refilled with something nobody chose.


6. The group, for notifications

Separate from the Mini App and needed for bookings, orders and room requests.

  1. Create a Telegram group and turn on Topics (Forum mode) in group settings.
  2. Add the bot and make it an administrator with Manage Topics.
  3. In the group, send /start (or /login) and follow the prompts.

The app creates topics itself as they are needed:

TopicCarries
<HOTEL>-HUBMenu orders and guest reports
<HOTEL>-DESKExpress checkout requests
<HOTEL>-HOUSECleaning requests and Do Not Disturb
<HOTEL>-<SERVICE>Bookings for that service

Inline “open the Mini App” buttons only work in private chats. Telegram rejects the entire message if a web_app button appears in a group, which would take the notification down with it. Group messages therefore use an ordinary link button to the same place. Nothing to configure — but it explains why the two look slightly different.


7. Commands worth registering

Optional — the bot answers these whether or not they appear in the menu — but registering them gives users autocomplete. /mybots → Edit Commands:

app - Open Bronit orders - Today's orders active-orders - Orders in progress income - Today's income statistics - Revenue breakdown

Content orders adds these in a private chat with the bot, for staff whose company has the module:

today - Today's priority list status - All contracts, or /status @Client contracts - Active contracts add - /add 5 @Client — add to the buffer published - /published @Client — mark delivered report - /report @Client — Excel report

And these in a client group, after /link @ClientName:

mystatus - Delivery progress contract - Contract summary buffer - What is ready to publish

8. Check it works

In order, because each step depends on the one before:

  1. npm run telegram:webhook -- status — URL correct, no last error, pending count near zero.
  2. Message the bot /start in a private chat. You should get a button.
  3. Press it. The Mini App opens; an unlinked account is shown the linking steps from §5 rather than a form.
  4. In the connected group, /orders. A report should come back.
  5. Scan a room’s QR code, or open t.me/<bot>/<app>?startapp=guest_<hotel>_<room> — it should land on that room’s menu, not the hub.

When something is wrong

SymptomCause
Bot silent everywhereTELEGRAM_BOT_TOKEN unset, or the webhook not registered
Messages arrive, buttons do nothingcallback_query missing from allowed_updates — re-run set
Nothing reaches the app, no errors visibleTELEGRAM_WEBHOOK_SECRET changed in .env but not re-sent to Telegram
Notifications arrive with no buttonTELEGRAM_MINIAPP_NAME unset — the most common one
Mini App opens to a blank pageNEXT_PUBLIC_APP_URL wrong, or the URL in BotFather points elsewhere
“Open this from Telegram”The page was opened in an ordinary browser; it needs Telegram’s signature
Group message fails to send entirelyA web_app button reached a group — a bug, not configuration
Topics not being createdBot is not an admin, or the group does not have Topics enabled

Framing is already configured in code: next.config.ts sets frame-ancestors to allow t.me and web.telegram.org. If the Mini App refuses to load in an iframe, that header is where to look — not BotFather.