From Chaos to Order: Centralizing Telegram Bot Chat Management

Adding Chat Management to a Telegram Bot: When One Database Is Better Than Ten
The Telegram bot was humming along nicely with user management working like a charm. But as the feature set grew, we hit a wall: there was no persistent way to track which chats the bot actually manages, who owns them, or what settings apply to each. Everything either lived in memory or was scattered across request handlers. It was time to give chats their own home in the database.
The project already had solid infrastructure in place. UserManager was handling user persistence using aiosqlite for async SQLite access, with everything stored in data/agent.db. The decision was simple but crucial: don’t create a separate database or fragment the data layer. One database, one source of truth, one connection pattern. Build on what’s already working.
First thing I did was design the schema. The managed_chats table needed to capture the essentials: a chat_id as the primary key, owner_id to link back to users, chat_type to distinguish between private conversations, groups, supergroups, and channels. I added a title field for the chat name and threw in a JSON column for future settings—storing metadata without needing another schema migration down the road. Critical detail: an index on owner_id. We’d be querying by owner constantly to list which chats a user controls. Full table scans would kill performance when the chat count climbed.
Rather than over-engineer things with an abstract repository pattern or some elaborate builder, I mirrored the UserManager approach exactly. Same dependency injection style, same async/await patterns, same connection handling. The ChatManager got three core methods: add_chat() to register a new managed chat, is_managed() to check if the bot should handle events from it, and get_owner() to verify permissions. Every query used parameterized statements—no room for SQL injection to slip through.
The interesting part was how SQLite’s INSERT OR REPLACE behavior naturally solved an edge case. If a chat got re-added with different metadata, the old entry simply disappeared. Wasn’t explicitly planned, just fell out from using chat_id as the primary key. Sometimes the database does the right thing if you let it.
Here’s something most developers overlook: SQLite gets underestimated in early-stage projects. Teams assume it’s a toy database, good only for local development. In reality, with proper indexing, parameterized queries, and connection discipline, SQLite handles millions of rows efficiently. The real issue comes later when projects outgrow the single-file limitation or need horizontal scaling—but that’s a different problem entirely, not a fundamental weakness of the engine.
The result was clean architecture: one database, one connection pool, new functionality integrated seamlessly without duplicating logic. ChatManager sits comfortably next to UserManager, using the same libraries, following the same patterns. When complex queries become necessary, the index is already there. When chat settings need expansion, JSON is waiting. No scattered state, no microservice overkill, no “we’ll refactor this later” debt.
Next comes integrating this layer into Telegram’s event handlers. But that’s the story for another day.
😄 Why did the SQLite database go to therapy? It had too many unresolved transactions.
Metadata
- Session ID:
- grouped_C--projects-bot-social-publisher_20260209_1143
- Branch:
- main
- Dev Joke
- Разработчик: «Я знаю ArgoCD». HR: «На каком уровне?». Разработчик: «На уровне Stack Overflow».