ENยทESยทDEยทPTยทFR
โŒ˜K

Self-Hosting the Terminal Broker

The terminal feature lets you run CLI agents (Claude, Codex, Gemini) directly from Telegram. By default it uses the shared hosted broker at terminal.aidaemon.ai. Self-hosting gives you full control over the relay infrastructure.

Architecture

The terminal system has three components:

  • aidaemon daemon (Rust) — runs on your machine, spawns PTY sessions, bridges to the broker via WebSocket
  • Broker (Cloudflare Worker) — WebSocket relay between the Mini App and the daemon. Handles auth, rate limiting, and session management
  • Mini App (embedded in broker) — Telegram Mini App frontend with xterm.js terminal emulator
architecture
Telegram โ”€โ”€> Mini App (browser) <โ”€โ”€WSSโ”€โ”€> Broker (CF Worker) <โ”€โ”€WSSโ”€โ”€> aidaemon daemon โ”€โ”€> local PTY

                                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚  Cloudflare        โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ Telegram โ”‚โ”€โ”€โ”€>โ”‚ Mini App         โ”‚<โ”€>โ”‚  Worker (Broker)   โ”‚<โ”€>โ”‚ aidaemon       โ”‚โ”€โ”€>โ”‚ local PTY โ”‚
  โ”‚          โ”‚    โ”‚ (xterm.js)       โ”‚   โ”‚  - auth            โ”‚   โ”‚ (terminal      โ”‚   โ”‚ (bash,    โ”‚
  โ”‚          โ”‚    โ”‚                  โ”‚   โ”‚  - rate limiting   โ”‚   โ”‚  bridge)       โ”‚   โ”‚  zsh...)  โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚  - session mgmt   โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         WSS             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          WSS
End-to-End Encryption
The broker is a transparent relay — it never sees the actual terminal content. All terminal I/O is encrypted between the Mini App and your daemon.

Prerequisites

  • Cloudflare account — free tier is sufficient for personal use
  • A Telegram bot — created via @BotFather
  • aidaemon installed and running — with the Telegram channel configured (Telegram setup guide)
  • Node.js 18+ and npm — for the wrangler CLI

Step 1 — Clone the Broker

The broker is a standalone Cloudflare Worker project. Clone it and install dependencies:

bash
git clone https://github.com/davo20019/terminal.aidaemon.ai.git
cd terminal.aidaemon.ai
npm install

Step 2 — Configure Deployment

Open wrangler.toml and choose how you want to deploy:

Option A: workers.dev Subdomain (Simplest)

If you don't have a custom domain, use Cloudflare's free *.workers.dev subdomain. Comment out or remove the routes block:

wrangler.toml (workers.dev)
name = "terminal-broker"
main = "src/index.js"
compatibility_date = "2024-12-05"
compatibility_flags = ["nodejs_compat"]

# No routes block = uses your-name.workers.dev
# Your broker URL will be: https://terminal-broker.<your-subdomain>.workers.dev

[durable_objects]
bindings = [
  { name = "TERMINAL_SESSION", class_name = "TerminalSession" }
]

[[migrations]]
tag = "v1"
new_classes = ["TerminalSession"]

Option B: Custom Domain

If you have a domain on Cloudflare DNS, set up a route to it:

wrangler.toml (custom domain)
name = "terminal-broker"
main = "src/index.js"
compatibility_date = "2024-12-05"
compatibility_flags = ["nodejs_compat"]

routes = [
  { pattern = "terminal.yourdomain.com", zone_name = "yourdomain.com" }
]

[durable_objects]
bindings = [
  { name = "TERMINAL_SESSION", class_name = "TerminalSession" }
]

[[migrations]]
tag = "v1"
new_classes = ["TerminalSession"]
Cloudflare DNS Required
Custom domains must be managed by Cloudflare DNS (orange-cloud proxied). If your domain is on another registrar, you can either transfer it or add it as a Cloudflare zone.

Step 3 — Set Secrets

The broker needs a few secrets to operate. Set them using the wrangler CLI:

bash
# Required: signs all authentication tokens
npx wrangler secret put SESSION_SIGNING_KEY
# paste output of: openssl rand -base64 32

# Required: validates Telegram user identity
npx wrangler secret put TELEGRAM_BOT_TOKEN
# paste your bot token from @BotFather

# Optional: protects admin API endpoints
npx wrangler secret put ADMIN_API_KEY
# paste a strong random string
Protect Your Signing Key
The SESSION_SIGNING_KEY signs all authentication tokens. If it is compromised, an attacker could forge sessions. Generate a strong random value and store it securely. Rotate it immediately if you suspect a leak.

Full list of available secrets:

KeyTypeDefaultDescription
SESSION_SIGNING_KEYstringRequired. Signs auth tokens. Generate with openssl rand -base64 32
TELEGRAM_BOT_TOKENstringRequired. Bot token from @BotFather. Must match the token in your aidaemon config
ADMIN_API_KEYstringOptional. Protects admin endpoints (user management, quota overrides)
BILLING_WEBHOOK_SECRETstringOptional. HMAC secret for billing webhook signature verification

Step 4 — Deploy

Deploy the broker to Cloudflare Workers:

bash
npx wrangler deploy

You should see output like this:

deploy output
Uploaded terminal-broker (3.45 sec)
Published terminal-broker (0.35 sec)
  https://terminal-broker.your-subdomain.workers.dev
Current Deployment ID: abc12345-...
Durable Object Migration
The first deploy automatically creates the Durable Object migration for TerminalSession. No manual migration steps are needed.

Step 5 — Configure aidaemon

Point your aidaemon daemon at your self-hosted broker. Update your config.toml:

config.toml
[terminal]
web_app_url = "https://your-broker.your-domain.com"
daemon_ws_url = "wss://your-broker.your-domain.com/v1/ws/daemon"

Or use environment variables:

bash
export AIDAEMON_TERMINAL_WEB_APP_URL="https://your-broker.your-domain.com"
export AIDAEMON_TERMINAL_BROKER_URL="wss://your-broker.your-domain.com/v1/ws/daemon"

Restart aidaemon after changing the configuration for the new values to take effect.

Step 6 — Configure Telegram

How you configure Telegram depends on how you launch the terminal:

Via /terminal Command (Inline Buttons)

If you use the /terminal command in your bot chat, the Mini App URL comes from web_app_url in your config.toml. No BotFather changes are needed — the inline button URL is set by aidaemon automatically.

Via Menu Button

If you want the terminal accessible as a permanent menu button in your bot:

  1. Open @BotFather on Telegram
  2. Send /mybots and select your bot
  3. Go to Bot SettingsMenu Button
  4. Set the URL to your broker (e.g. https://terminal-broker.your-subdomain.workers.dev)
HTTPS Required
Telegram requires the Mini App URL to use HTTPS. Both workers.dev subdomains and Cloudflare-proxied custom domains provide HTTPS automatically.

Verification

Confirm everything is working:

1. Health Check

bash
curl https://your-broker.your-domain.com/health

Expected response:

health response
{
  "ok": true,
  "service": "your-broker.your-domain.com",
  "version": "1.0.0",
  "timestamp": "2026-03-01T12:00:00.000Z"
}

2. Test the Mini App

Send /terminal to your bot in Telegram. The Mini App should open and display the terminal interface.

3. Check aidaemon Logs

Look for the terminal bridge connection in your aidaemon logs:

aidaemon logs
[INFO] Connecting terminal bridge to broker: wss://your-broker.your-domain.com/v1/ws/daemon
All Set
If the health check returns "ok": true, the Mini App opens in Telegram, and aidaemon logs show the broker connection — your self-hosted terminal is fully operational.

Quota Configuration

The broker enforces per-user monthly quotas to prevent abuse. It tracks session starts, active minutes, bytes transferred, and WebSocket frames. The default "free" plan limits are generous and more than sufficient for personal use.

To customize the plan limits, set the PLAN_LIMITS_JSON environment variable in your wrangler.toml:

wrangler.toml
[vars]
PLAN_LIMITS_JSON = '{"free":{"maxSessionsPerMonth":500,"maxMinutesPerMonth":3000,"maxBytesPerMonth":536870912,"maxFramesPerMonth":500000}}'

To set a specific user to the "pro" plan (or any custom plan), use the admin API:

bash
curl -X POST https://your-broker.your-domain.com/admin/users/TELEGRAM_USER_ID/plan \
  -H "Authorization: Bearer YOUR_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan": "pro"}'

Available plan limit keys:

KeyTypeDefaultDescription
maxSessionsPerMonthinteger100Maximum terminal sessions a user can start per month
maxMinutesPerMonthinteger600Maximum active terminal minutes per month
maxBytesPerMonthinteger104857600Maximum bytes transferred per month (default ~100 MB)
maxFramesPerMonthinteger100000Maximum WebSocket frames per month

Troubleshooting

SymptomCauseFix
"Terminal bridge disabled" in aidaemon logsdaemon_ws_url is misconfiguredMust use wss:// (not ws://). Cloudflare Workers require TLS.
Mini App shows "invalid session"Token mismatchTELEGRAM_BOT_TOKEN in the broker must exactly match the bot token in your aidaemon config.toml
"Token signature mismatch"Missing or wrong signing keyEnsure SESSION_SIGNING_KEY is set via npx wrangler secret put
WebSocket connection dropsWorker error or timeoutCheck the Cloudflare dashboard → Workers & Pages → your worker → Logs for errors
"daemon_auth_rate_limited"Too many failed connection attemptsWait 60 seconds, then fix the underlying auth issue and reconnect

Security Notes

  • The broker is a transparent relay — terminal content is end-to-end encrypted between the Mini App and your daemon
  • The broker sees: user IDs, session metadata, and encrypted frame sizes — but never the actual terminal content
  • The SESSION_SIGNING_KEY is the most critical secret — rotate it immediately if compromised
  • All connections use TLS (WSS) — Cloudflare Workers enforce HTTPS by default
Full Threat Model
For a detailed security analysis, see the SECURITY.md in the broker repository.