How this runs in production
The hosted demo you are using runs entirely in your browser. This page documents the intended server architecture that would run real Telegram bots outside of this demo. It is a blueprint only - nothing here is connected in this hosted build.
FastAPI service
A single FastAPI + asyncio service hosts every bot. It exposes a REST API for the builder (CRUD on flows), a WebSocket endpoint for live logs, and the Telegram webhook receiver.
# server/main.py
from fastapi import FastAPI
from .runtime import BotRuntimeManager
app = FastAPI(title="Bot Maker Runtime")
manager = BotRuntimeManager()
@app.post("/webhook/{bot_id}")
async def webhook(bot_id: str, update: dict):
return await manager.dispatch(bot_id, update)
@app.get("/api/bots/{bot_id}/logs")
async def logs(bot_id: str):
return await manager.logs(bot_id)SQLite storage
Flows, tokens, variables and message history live in a single SQLite database. Tables are created idempotently on first run.
CREATE TABLE IF NOT EXISTS bots (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
token TEXT,
status TEXT DEFAULT 'draft',
flow_json TEXT NOT NULL,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_id TEXT NOT NULL,
chat_id TEXT,
from_user TEXT,
text TEXT,
ts INTEGER
);Dynamic bot runtime manager
A runtime manager starts, pauses and stops one bot worker per project. Each worker runs on aiogram (or the raw Bot API) and executes the flow graph from flow_json - the same block model this builder edits.
# server/runtime.py
class BotRuntimeManager:
def __init__(self):
self.workers: dict[str, BotWorker] = {}
async def start(self, bot_id: str):
bot = await db.get_bot(bot_id)
self.workers[bot_id] = BotWorker(bot)
await self.workers[bot_id].start()
async def stop(self, bot_id: str):
await self.workers[bot_id].stop()
self.workers.pop(bot_id, None)Polling and webhooks
Bots can run in either mode. Polling is simplest for development; webhooks are recommended for production so Telegram pushes updates straight to the service.
# polling
worker = BotWorker(bot)
await worker.start_polling()
# webhook
url = f"https://bot.example.com/webhook/{bot_id}"
await api.set_webhook(url)Real-time logs
Each worker streams incoming messages and node-step events to a WebSocket channel, mirroring the execution log you see in the in-browser simulator.
async def stream_logs(websocket, bot_id):
async for event in manager.subscribe(bot_id):
await websocket.send_json(event)systemd and Caddy
The service runs as a systemd unit behind Caddy, which terminates TLS and reverse-proxies the app. This is the layer that would serve the builder at a path like /botmaker.
# /etc/systemd/system/botmaker.service
[Unit]
Description=Bot Maker Runtime
After=network.target
[Service]
User=botmaker
WorkingDirectory=/opt/botmaker
ExecStart=/opt/botmaker/.venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8003
Restart=always
[Install]
WantedBy=multi-user.target# Caddyfile
bot.example.com {
reverse_proxy /botmaker/* 127.0.0.1:8003
reverse_proxy /webhook/* 127.0.0.1:8003
}The builder already produces a portable JSON flow model that this runtime consumes directly. The block palette, variables, HTTP configuration, conditions, branching, wait-for-reply and AI prompts all map one-to-one onto the runtime's execution engine, so moving from this hosted demo to a real deployment is a server-side concern, not a redesign.