Production blueprint

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.

Not connected. This hosted demo simulates deploys and bot execution in the browser. To run a real bot you would deploy the FastAPI service described below on your own server and paste a real BotFather token.

FastAPI service

port 8003

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

bots.db

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

aiogram / raw Bot API

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

two modes

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

WebSocket

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

deployment

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
}
API readiness

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.