tg-mini-app-emulator
v0.1.1
Published
Local Telegram Mini App emulator — run mini-apps without ngrok or a real bot.
Maintainers
Readme
tg-mini-app-emulator
Run Telegram Mini Apps locally — no ngrok, no real bot, no public URL.
Prerequisites
- Node.js ≥ 20 (CLI runs on plain Node; Bun not required)
- A locally running mini-app dev server (any localhost port)
Quick start
# 1. Run your mini app dev server (any localhost port)
npm run dev # e.g. http://localhost:5173
# 2. Run the emulator pointing at it
npx tg-mini-app-emulator --target http://localhost:5173
# 3. Open http://localhost:1234In your mini-app HTML, load the emulator SDK (dev only):
<script src="http://localhost:1234/sdk.js"></script>Use query ?emu=http://localhost:PORT on your mini-app URL when the emulator listens on a non-default port.
Production stays on https://telegram.org/js/telegram-web-app.js.
Forward sendData to your bot's webhook
npx tg-mini-app-emulator \
--target http://localhost:5173 \
--webhook http://localhost:3000/telegram/webhook \
--webhook-secret your-secret \
--bot-token 123456:ABC...Bot token alignment: the emulator HMAC-signs
initDatawith the bot token you pass. Your backend must verify with the same token, or it will reject the launch asinvalid_init_data. In dev, prefer--bot-token-env ./path/to/.envto readTELEGRAM_BOT_TOKENfrom a single source of truth.
Simulating real launch sources (channel inline button vs attachment menu)
Real Telegram delivers different initData shapes per entry point. The emulator simulates this via launchSource:
| launchSource | Real-world trigger | initData includes | Channel id source |
| ------------------------- | ------------------------------------------ | ------------------------------------------------ | ------------------------------------------ |
| attachment_menu (default) | 📎 menu inside chat | chat (id/type/title) | chat.id |
| direct_link | Channel post inline button, t.me/bot/app | chat_type + chat_instance (no chat.id) | bot encodes channel_id into start_param|
direct_link requires chatType. chatInstance and queryId are optional.
Config file (multi-profile)
npx tg-mini-app-emulator --config examples/tg-emu.config.json --profile channel_inlineA complete runnable example ships with the package — see examples/tg-emu.config.json after install. Bundled profiles:
| Profile | Launch source | Notes |
| ------------------------- | -------------------- | -------------------------------------------------------- |
| channel_inline | direct_link | Demo channel via start_param=channel_id=DEMO_CH_001 |
| attachment_menu_channel | attachment_menu | Channel chat with chat.id |
| vip_direct_link | direct_link | Second demo identity for testing |
You may omit --target when the config file provides one.
Profile fields
| Field | Required | Description |
| -------------- | ------------------ | ------------------------------------------------------------------ |
| user | yes | TgUser object, at minimum id + first_name |
| chat | yes | Used for webhook forward; not injected into initData when launchSource=direct_link |
| launchSource | no | attachment_menu (default) / direct_link |
| chatType | yes for direct_link| private / group / supergroup / channel / sender |
| chatInstance | no | Stable hash representing source chat (direct_link scenarios) |
| queryId | no | For answerWebAppQuery |
| startParam | no | Maps to t.me/bot/app?startapp=...; recommended key=value format|
| theme | no | light / dark |
CLI
| Flag | Required | Description |
| ------------------------ | -------- | -------------------------------------------------------------------------------------------- |
| --target <url> | yes* | Mini app dev URL (localhost / 127.0.0.1 / *.local) |
| --webhook <url> | no | Forward sendData / callback as Telegram Update JSON |
| --webhook-secret <s> | no | Sets x-telegram-bot-api-secret-token header |
| --bot-token <t> | no | Bot token for HMAC initData signing |
| --bot-token-env <path> | no | Read TELEGRAM_BOT_TOKEN from this .env file; explicit --bot-token takes precedence |
| --port <n> | no | Emulator port (default 1234) |
| --config <path> | no | JSON config path |
| --profile <name> | no | Active profile |
| --no-open | no | Do not open browser |
*Not required if --config provides target.
SDK coverage (B-tier)
ready/expand/closeinitData/initDataUnsafeMainButton,BackButtonthemeParams/colorScheme+themeChangedviewportHeight/viewportStableHeight/isExpanded+viewportChangedHapticFeedback(log only in emulator)sendData→ webhook forward asweb_app_dataonEvent/offEvent
Examples
The package ships a runnable minimal mini-app at examples/minimal-mini-app/ (after install: node_modules/tg-mini-app-emulator/examples/minimal-mini-app/). It exercises MainButton, BackButton, HapticFeedback, themeChanged, and start_param parsing.
To try it end-to-end:
# Terminal 1 — serve the example mini-app
npx serve node_modules/tg-mini-app-emulator/examples/minimal-mini-app -l 5173
# Terminal 2 — emulator pointing at it, with a profile
npx tg-mini-app-emulator \
--config node_modules/tg-mini-app-emulator/examples/tg-emu.config.json \
--profile channel_inlineTroubleshooting
iframe shows "session expired / please reopen from Telegram" (API responds 401)
The emulator-signed initData HMAC does not match the bot token your backend verifies with. Use --bot-token-env pointing to your backend's .env, or pass the same token explicitly via --bot-token.
Verify the cookie flow with curl (replace endpoint and cookie name as appropriate):
curl -i -X POST http://<your-backend>/entry \
-H "content-type: application/json" \
-d "{\"initData\":\"$(curl -s http://localhost:1234/api/init-data | jq -r .initData)\"}"
# Expected: 302 + Set-CookieMainButton / BackButton don't trigger your handler
Make sure the SDK script tag uses the emulator origin in dev (http://localhost:1234/sdk.js) instead of https://telegram.org/js/telegram-web-app.js. Production must use the real URL.
Not covered (future)
CloudStorage,BiometricManagershowPopup/showAlert/showConfirmopenInvoice/openLinkSettingsButton,SecondaryButton
License
MIT
