Web App (Admin SPA + Telegram Mini-App)¶
Since v0.10 XTV-SupportBot ships a React Single-Page App that is served directly from the FastAPI process. It powers two surfaces from the same bundle:
- Admin console — open
https://<your-domain>/in a regular browser, paste anxtv_…API key, and get a full operations UI (Overview / Inbox / Ticket detail / Projects / Rules). - Telegram Mini-App — opened by tapping an Open App inline
button or the menu-button in any chat. The page authenticates
against Telegram's signed
initData, so the user is logged in automatically and scoped to their own tickets.
The UI_MODE environment variable picks which entry points the bot
exposes: classic inline buttons (chat), Mini-App only (webapp),
or both side-by-side (hybrid).
Architecture at a glance¶
┌──────────────────────────────┐
HTTPS request ─────▶│ FastAPI (uvicorn, same PID │
│ as the Telegram bot) │
│ │
│ /api/v1/* ← JSON │
│ /assets/* ← SPA bundle │
│ / ← index.html │
│ /<any> ← index.html │
│ (SPA fallback) │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ React SPA (/web/) │
│ │
│ desktop browser → Admin UI │
│ in Telegram → User UI │
└──────────────────────────────┘
- One process, one port. Railway / Render / Fly inject
$PORTwhich the bot picks up viaeffective_api_port; the SPA lives at the same origin as the JSON API so CORS is a non-issue. - One bundle, two UIs.
main.tsxasksGET /api/v1/meon the first render; if the caller is an admin on desktop it mounts the fullAdminLayout, otherwise it mounts the Mini-App-friendlyUserLayoutwith a bottom tab-bar.
Modes¶
UI_MODE in .env:
| Mode | Inline buttons | Open-App button | When to pick |
|---|---|---|---|
chat (default) |
✅ | — | You don't want to publish a Mini-App. 100% backward-compatible. |
webapp |
— | ✅ | Clean one-button UX. Every interaction happens inside the SPA. |
hybrid |
✅ | ✅ | Transitioning from chat → webapp; lets users choose per tap. |
Per-user override: users.ui_pref (chat / webapp / hybrid)
beats the global. Set it from the Mini-App's Settings → UI
preference screen.
Graceful fallback¶
Even in webapp / hybrid mode the bot downgrades to chat
silently when:
WEBAPP_URLis empty or nothttps://- the user's Telegram client is
< 6.0(WebApps shipped April 2022) - a Mongo hiccup prevents the user-pref lookup
So a misconfigured deploy never emits a broken keyboard.
Authentication¶
Desktop admin¶
The primary login is a username/password account with a server-side session cookie — see Admin accounts. A logged-in account's permissions are resolved from the existing Role system.
The legacy Authorization: Bearer xtv_<40-char-key> login still works as
a secondary path; keys are created via /apikey create in the bot and
persisted SHA-256 at rest, plaintext shown once. See
API auth for the full key lifecycle.
Telegram Mini-App¶
X-Telegram-Init-Data: user=%7B...%7D&auth_date=…&hash=…
The initData query string is signed with
HMAC-SHA256(key="WebAppData", msg=BOT_TOKEN). The server
validates the signature, rejects auth_date older than 24 hours,
and decodes the user JSON into a typed TelegramUser. See
src/xtv_support/api/auth_webapp.py for the implementation.
The shared FastAPI dependency current_principal resolves the caller in
precedence order: initData → session cookie → bearer key.
Account & profile menu¶
The admin console footer is an account chip — avatar (Telegram photo
when available, otherwise coloured initials; a key glyph for legacy
API-key sessions), display name, and role. Clicking it opens a dropdown
with a theme switch (Auto / Light / Dark, persisted in
localStorage and applied before first paint), Account settings,
quick links to Accounts and API Keys (admins only), and Log out.
The Account page (/admin/account) shows the caller's identity and
theme preference and — for username/password logins — a change
password form. Changing the password verifies the current one and
signs out every other device (POST /api/v1/auth/change-password).
On phones the left rail is replaced by a bottom navigation bar — the primary destinations (Overview · Queue · Projects), a MENU button that opens a full-navigation sheet with every section, and the profile chip on the right. The bottom bar respects the iOS safe-area inset.
/api/v1/me — user-scoped endpoints¶
Every route below is scoped to the caller — a user can never read or mutate another user's ticket.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/me |
Profile, is_admin, role, auth_method (telegram/account/apikey), ui_mode, brand strings |
GET |
/api/v1/me/projects |
Active projects available for intake |
GET |
/api/v1/me/tickets?status=… |
Caller's tickets with filter |
GET |
/api/v1/me/tickets/{id} |
Ticket + history (internal notes stripped) |
GET |
/api/v1/me/languages |
Supported UI languages (code/name/flag), auto-discovered from bundled locales |
POST |
/api/v1/me/tickets |
Create a new ticket (creates the admin-supergroup forum topic, same as the bot) — rate-limited per the shared cooldown |
POST |
/api/v1/me/tickets/{id}/attach |
Upload an image/file attachment (stored via the bot in the supergroup) |
GET |
/api/v1/me/tickets/{id}/attachments/{index} |
Stream one of the caller's own attachments |
POST |
/api/v1/me/tickets/{id}/reply |
Add a user reply |
POST |
/api/v1/me/tickets/{id}/close |
Self-close |
GET |
/api/v1/me/settings |
Language + notification prefs + ui_pref |
PATCH |
/api/v1/me/settings |
Partial update |
Admin routes (/api/v1/tickets, /projects, /rules, /analytics,
/webhooks) keep their existing API-key + scope model — see
API reference.
SPA routes¶
Every path is served by index.html so deep-links survive a hard
refresh.
User¶
/— Home (greeting, stats, quick actions)/new— New ticket (project picker + message)/tickets— My tickets (filter chips)/tickets/:id— Chat-style thread, reply, self-close/settings— Language / notifications / UI preference
Admin¶
/admin— Overview tiles/admin/inbox— Saved-view chips + ticket table/admin/tickets/:id— Thread + tag editor + reply / close / reopen/admin/projects— List + create-from-template dialog/admin/rules— Full rule builder (create / edit / toggle / delete)
Enabling the Mini-App¶
- Deploy with
UI_MODE=webapp(orhybrid) and a validWEBAPP_URL=https://<your-domain>/. - Configure the WebApp domain in @BotFather → Bot Settings → Configure Mini App and point it at the same URL.
- Optional: set
WEBAPP_SET_MENU_BUTTON=trueso the bot callssetChatMenuButtonat boot and every chat gets a persistent "Open App" button next to the text composer.
See Deploy the Web App for the full Railway walkthrough.
Rolling back¶
- Instantly disable the Mini-App:
UI_MODE=chat+ redeploy. - Disable the SPA mount but keep the API:
WEB_ENABLED=false. - Nothing to undo on the DB — there are no Mini-App-specific collections or migrations.