Architecture¶
This page is the map you read before the code. It follows one Telegram update from the moment it lands on the webhook to the moment the bot replies, and names the object responsible at each hop. Once you can trace this path, most of the codebase reads in order.
The update flow¶
Every interaction, a tapped button, a typed command, a shared location, arrives as a single Telegram update and walks the same path:
Two properties are worth holding onto. The webhook answers 204 for every well-formed request, even one it cannot parse, so Telegram never retries a poison-pill update in a tight loop. And the endpoint hands off to the queue instead of processing inline, so the HTTP request returns in milliseconds while the handler runs on its own.
MitupRuntime, the composition root¶
apps/bot/mitup_bot/app.py holds MitupRuntime, the single place where the whole bot is wired together. Its constructor builds the config, configures logging, metrics, the database, and the timezone API, then assembles the PTB Application: the bot token, the custom MitupContext, the rate limiter, and the PerUserUpdateProcessor. HandlersRegistry.bind(app) registers every handler onto that application. run() picks polling or webhook mode from config and starts the server. Nothing else constructs these pieces, so if you want to know what depends on what, start here.
The webhook host¶
apps/bot/mitup_bot/web/ holds the FastAPI app factory and the POST /telegram route. The runtime serves it through uvicorn with workers=1. That single worker is not an oversight: the PTB Application owns in-memory state (conversation states, per-user data) that cannot be shared across processes, so exactly one worker may exist. The endpoint validates the Telegram secret header, parses the update, puts it on app.update_queue, and returns.
Concurrency in one paragraph¶
Because there is one worker, all concurrency lives inside that one event loop. PTB's own semaphore caps how many updates run at once through bot.concurrent_updates, which defaults to 1 and keeps processing observably sequential. PerUserUpdateProcessor adds a second guarantee: updates that share the same (user, chat) key always run one at a time, so a user's own actions never race each other even when the cap is raised above 1. In-process locks are enough precisely because there is only ever one worker holding them.
Going deeper on the web layer
This page stays at map altitude. For the FastAPI app factory, the lifespan that owns PTB's initialize/start/set_webhook/stop/shutdown sequence, secret-token validation, and the hard rules that keep the webhook from breaking silently, read the web-conventions skill.