Architecture
Three layers (apps / core / infrastructure), data flow from MTProto Update to delivered notification, multi-tenancy via RLS.
Read
CLAUDE.mdfirst. This page expands on the layered architecture and the data flow from MTProto Update to delivered notification.
Layers
apps/ ──→ entrypoints (bot, api, miniapp, userbot, workers)
core/ ──→ pure domain logic, policies, settings, events — NO I/O
infrastructure/ ──→ DB, Redis, storage, search, crypto, HTTP adapters
plugins/ ──→ payments, enrichment, delivery — Protocol-based
shared/ ──→ cross-cutting utilities (logging, redaction, time, metrics)
i18n/ ──→ ICU translations
config/ ──→ seed defaults for the parameters registry
core/ knows nothing about Postgres, Redis, or aiogram. Adapters in
infrastructure/ implement Protocols defined in core/. Tests in
tests/unit/ run without any container.
Data flow
┌─────────────┐
│ userbots │ pyrotgfork Clients wrapped by stealth.guard
└──────┬──────┘
│ Update* events
▼
┌─────────────┐
│ Redis Streams│
└──────┬──────┘
▼
┌─────────────┐
│ ingester │ ──→ Postgres (TimescaleDB hypertables)
└──────┬──────┘
▼
┌─────────────┐
│ aggregator │ ──→ Meilisearch index (multi-tenant wrapped)
│ analytics │
└──────┬──────┘
▼
┌─────────────┐ ┌─────────────┐
│ logging_router│ ─→ ─→ │ delivery/* │ ─→ bot / channel / webhook / ...
└─────────────┘ └─────────────┘
Multi-tenancy
Every client-scoped table has a client_id column and a Postgres RLS
policy keyed on current_setting('app.client_id'). The GUC is set by
apps/bot/middlewares/context.py and apps/api/middlewares/....
Bypassing RLS is a P0 incident.
Search uses infrastructure/search/meilisearch.py which always injects
client_ids = <client_id> into every query.
Settings registry
Every value that could differ between environments, tiers, clients,
targets, chats, routes, userbots, or personas is a Parameter in
core/settings/parameters.py. Reads are O(1) once warmed (in-memory LRU
keyed by (key, scope_ctx)). Writes go to settings_overrides and
broadcast a Redis pub/sub event that invalidates every process's cache.
Stealth model
The userbot cluster has exactly three visibility classes for Pyrogram methods:
- Zero-visibility (allowed):
import_contacts,getFullUser,getUser,getCommonChats, receivingUpdateUserStatus,getHistoryin a shared chat, media download. - Passive-visibility (allowed only if persona is plausible): userbot present in member list, mutual contacts, userbot's own online status.
- Active-visibility (ABSOLUTELY FORBIDDEN): any RPC that notifies the target. See Stealth thesis.