Telegram SaaS Framework
A framework for building production-ready Telegram bots in one evening
Telegram SaaS Base is a private framework I use to launch my own Telegram products. 28+ ready-made modules, built-in multi-tenancy, 5 payment providers, AI integration, and Web Mini App support in a single codebase.
About the project
Telegram SaaS Base is a private personal framework I built to launch my own Telegram products quickly. Every time I need a new bot, I don’t start from scratch: I create a token, register the bot in the admin panel, select the required modules, configure them through the UI and set the webhook. The first bot with payments, localization and analytics takes about three minutes.
The framework is built on Next.js 16 with App Router, React 19, PostgreSQL 16, Redis 7, BullMQ for queues and grammY for the Telegram Bot API. All code is typed with TypeScript. The architecture follows vertical slices: each module is isolated and owns its route handlers, database schema, admin panel UI components and message handlers. Adding a new module requires no changes to the core.
The same codebase operates simultaneously in two modes: as a native Telegram Bot with commands, inline keyboards and callbacks, and as a Telegram Web Mini App with a full React interface. The admin panel manages all bots from a single point and supports multi-tenancy via AsyncLocalStorage with no direct tenant_id queries anywhere in the business logic.
Core framework capabilities
Unified Dispatcher
A single entry point for everything: Telegram Bot API, Next.js HTTP routes and Web Mini App. One middleware stack, one authorization system, one source of truth for sessions and tenant context.
- Telegram webhook, polling and HTTP API are handled by a single pipeline
- Middleware executes once regardless of runtime (bot or web)
- Tenant and user context is injected automatically via AsyncLocalStorage
- Modules don't know which runtime they run in: one logic, different transports
Module Registry
Modules register automatically via a static registry on import. Connecting a new module is one line in the bot manifest. Removing it is the same line deleted. The framework core never changes when modules are added.
Multi-tenancy
Each bot is a separate tenant. AsyncLocalStorage holds the tenant context for the lifetime of a request. All business logic code is written without tenant_id: it is available automatically anywhere in the stack via ModuleContext.
Decorator-based routing
Command and callback routing via TypeScript decorators with Reflect Metadata. No manual handler registration, no switch-case on message text.
- @BotCommand('/start') registers the command in the bot automatically
- @CallbackQuery('action:*') supports wildcard pattern matching on callback data
- @BotAction() handles text messages with string or RegExp patterns
- Decorators stack: one method can respond to multiple triggers simultaneously
Payment layer
Strategy Pattern over 5 providers: Stripe, LiqPay, Monobank, Telegram Stars, CryptoMerchant. A new provider is added by implementing a single interface with no changes to module code.
AI integration
Two providers out of the box: OpenAI and Google Gemini. Any new AI provider takes 2 lines via a unified adapter. Full response typing, retry logic, rate-limit handling.
Telegram + Web Mini App
The same codebase serves the native bot with commands and inline keyboards and a full React Web Mini App. Mode switching without duplicating business logic.
AI localization
Translate the entire bot into any language in one click from the admin panel. AI generates translations, a human reviews. Supports any number of locales.
Technology stack
6 layers from the React UI to infrastructure. Each layer chosen for a specific purpose.
Unified Dispatcher: one pipeline for the bot and the web
The central architectural challenge of the framework is this: a Telegram Bot runs via webhook or polling, receiving Update objects from the Telegram API — a server process with its own event loop. A Web Mini App runs via browser HTTP requests — standard Next.js routing. The admin panel makes REST calls to the API. These are three fundamentally different transport layers with different request-response models, different authorization systems and different data formats.
The naive approach is three separate code paths, with duplicated business logic or a separate abstraction layer per transport. Telegram SaaS Base solves this differently: a single Dispatcher — the Adapter pattern — normalizes all incoming events to a unified internal format. Telegram Update, HTTP Request and WebSocket messages pass through one middleware stack before reaching the module’s business logic. This stack sets the tenant context via AsyncLocalStorage, checks authorization, resolves the user and writes to the audit log. All of this happens once, regardless of transport.
The consequence: modules are written with no transport awareness. The same payment processing method works whether it is called via a Telegram command or via the Web Mini App. No duplication, no if (isTelegram) in business code.
@BotCommand, @CallbackQuery, @BotAction
Command and callback routing via TypeScript decorators with Reflect Metadata. A method with a decorator is registered in the bot on module import. No manual event subscription, no switch-case on message text.
@Module('referral')
class ReferralModule extends BaseModule {
@BotCommand('/referral', 'Referral program')
async handleReferral(ctx: BotContext): Promise<void> {
const user = ModuleContext.getUser();
const stats = await this.referralService.getStats(user.id);
await ctx.reply(this.i18n.t('referral.stats', stats));
}
@CallbackQuery('referral:share')
async handleShare(ctx: BotContext): Promise<void> {
await ctx.answerCallbackQuery();
await ctx.reply(this.referralService.buildShareLink());
}
@BotAction(/^referral_code_(.+)$/)
async handleCode(ctx: BotContext): Promise<void> {
const code = ctx.match[1];
await this.referralService.applyCode(
ModuleContext.getUser().id,
code
);
}
}
Vertical slices: a module as a self-contained unit
Module architecture in Telegram SaaS Base follows the Vertical Slice pattern: each module contains everything it needs to operate. Not a service layer with shared repositories, not horizontal slicing by file type — a vertical slice from bot handlers through the database schema to admin panel UI components.
The module structure is fixed: handlers/ with decorated methods, services/ with business logic, schema.prisma with the database schema, admin/ with React components and admin API routes, i18n/ with translations, index.ts as entry point. When setting up a new bot, modules are connected via the manifest in one line. Database schemas are merged automatically by a merge script, then a single Prisma migration is run.
A practical consequence for development: when working on a specific module, you open one directory and see everything — handlers, logic, schema, UI. No need to jump between controllers/, services/, repositories/, models/ in different parts of the tree. This is especially valuable when moving or removing a module: it moves as a single package.
Modular database schema
Each module has its own schema.prisma file. Before migration a merge script joins all schemas into one, then a single migration runs. Tables from different modules don't share names: each table is prefixed by the module name.
// modules/referral/schema.prisma
model ReferralCode {
id String @id @default(cuid())
botId String
code String @unique
ownerId String
usageCount Int @default(0)
reward Int @default(0)
createdAt DateTime @default(now())
bot Bot @relation(fields: [botId], references: [id])
usages ReferralUsage[]
}
model ReferralUsage {
id String @id @default(cuid())
codeId String
userId String
createdAt DateTime @default(now())
code ReferralCode @relation(fields: [codeId], references: [id])
}
// After merge: npx prisma migrate dev
Onboarding
A new bot in 3 minutes
Create a bot in @BotFather
30 seconds
Send /newbot to @BotFather, choose a name and username. Receive the API token.
Add the bot in the admin panel
30 seconds
Paste the token into the registration form. The framework automatically pulls the bot name and avatar from the Telegram API.
Select and enable modules
1 minute
The UI shows all available modules with descriptions. A checkbox activates a module for this bot. Database schemas are merged automatically.
Configure modules
30 seconds
Each active module exposes a configuration form: texts, limits, payment keys, toggles. All through the UI without editing code.
Set the webhook
10 seconds
Click 'Set Webhook' in the admin panel. The framework registers the endpoint with Telegram and returns a confirmation status.
Bot in production
Done
The bot responds to commands, accepts payments and renders the Web Mini App. All data is collected in the admin panel in real time.
Multi-tenancy: one framework, many bots
When the framework serves multiple bots simultaneously, the classic multi-tenant problem arises: how to guarantee data isolation between tenants without business logic ever knowing multi-tenancy exists. Adding WHERE bot_id = $1 to every SQL query is not a solution — it is a source of bugs.
In Telegram SaaS Base, multi-tenancy is implemented via Node.js AsyncLocalStorage. When the Dispatcher receives an incoming request it identifies the tenant (bot) by token or request header, loads the tenant configuration and stores it in the AsyncLocalStorage store for the duration of the request. Any code within that request — module business logic, Payment Service, AI adapter — retrieves the tenant context via ModuleContext.getTenant() without any explicit argument.
The result: modules are written with no awareness of multi-tenancy. The referral service calculates stats — it simply works with data from the current tenant. The payment module selects a provider — from the current tenant’s configuration. No passing tenantId through call chains, no data leaks between bots. AsyncLocalStorage guarantees isolation at the event-loop level, even with parallel requests inside a single Node.js process.
Integrations
Payment providers
International bank cards, Apple Pay, Google Pay. Subscription model support, webhook processing, automatic retry on failures. The best fit for products with a global audience.
- Checkout Sessions and Payment Intents via Stripe API v2024
- Webhook signature verification, idempotency keys
- Full transaction audit log in PostgreSQL
Ukrainian payment provider: Visa/Mastercard, Google Pay, Apple Pay, cash-in terminals. Optimum for Ukrainian audiences with low transaction fees.
- LiqPay API v3: invoice creation, status check, callback
- HMAC-SHA1 webhook signature verification
Payment service from Ukraine's Monobank. Integration via the monopay API: creating payment links, tracking status callbacks.
- Invoice creation, redirect URL, webhook callback
Telegram's native payment system. Users buy Stars inside the app and use them to pay for bot services without leaving to a browser. Conversion rates are higher than external payment pages.
- sendInvoice API, pre_checkout_query handler, successful_payment handler
- Native UX without redirects outside Telegram
Accepting cryptocurrency payments via the CryptoMerchant API. Creating payment addresses, tracking confirmations, webhook on transaction completion.
- Multiple cryptocurrencies through a single adapter
AI integration: two providers out of the box, any new one in two lines
The framework ships with two AI providers: OpenAI (GPT-4o, GPT-4-Turbo and all models via a single API) and Google Gemini (Gemini 1.5 Pro and Flash). Both providers implement a common IAIProvider interface with complete(), chat() and embed() methods. A module that needs AI retrieves the provider via ModuleContext.getAI() — the tenant selects the provider in settings, while the module code has no dependency on any specific AI.
Adding a new provider means implementing the IAIProvider interface and registering the class in the factory. This takes roughly two lines in the configuration file. No changes to modules already using AI. Strategy Pattern in its pure form: the algorithm (provider) is swapped without changing the context (module).
The most impactful practical feature is AI-driven bot translation. The admin panel collects all localization strings from all active modules, sends them to AI with a system prompt to preserve formatting, receives translations and saves them to the database. The administrator reviews the result in a table and edits manually if needed. The entire bot is translated into a new language in a single request with no deployment.
Security
17 security measures
Localization: i18n as a first-class citizen
Every module in the framework contains an i18n/ directory with translation files in JSON format. Translation keys are typed: TypeScript knows all available localization keys for a module, autocomplete works in the IDE, and errors in keys are caught at compile time. On startup the application loads translations from all active modules and merges them into a single namespace partitioned by module name.
Calling a translation in code: this.i18n.t('referral.invite_text', { count: stats.referrals }). No magic strings, no runtime errors from missing keys. Supports interpolation, pluralization and nested keys. Language is resolved from the tenant’s settings, or from the Telegram user’s language preferences where needed.
AI translation is integrated into the admin panel: one button collects all strings from active modules, sends them to AI and saves the results. The entire bot is translated in a single request. A diff mechanism translates only the added or changed strings when modules are updated.
Infrastructure
Services and their roles
PostgreSQL 16
Primary store
40+ typed Prisma models, transactions, indexes on key fields. Each module contributes its own schema fragment via the merge script.
Redis 7 + BullMQ
Cache and workers
Session storage, rate limiting at the user and IP level, deferred and retryable jobs via BullMQ workers. Separate worker processes do not block the main event loop.
Next.js 16 (App Router)
HTTP server
HTTP API with all routes, server-side rendering for the React admin panel and Web Mini App. App Router provides Server Components, streaming and edge runtime where needed.
grammY
Telegram runtime
Incoming Update processing via webhook or polling. The grammY middleware stack is integrated with the Dispatcher: one pipeline for authorization, tenant context and routing to decorated module handlers.
S3-compatible storage
Object storage
User media files, data exported via the admin panel and database backups. The provider abstraction allows switching between AWS S3, MinIO or any S3-compatible service with no code changes.
Docker Compose
Single stack
Five services (app, worker, worker2, db, redis) defined in a single compose file. One docker compose up command brings up the full environment with hot reload for development or the production stack with proper resource limits.
Core subsystems
Module Registry
Singleton registry of all system modules. Automatically discovers modules on import via static registration. Provides access to module metadata: name, dependencies, active commands.
EventBus
Observer Pattern for cross-module communication. Modules publish events (payment.completed, user.registered) and subscribe to events from other modules without direct dependencies between them.
ModuleContext
Facade Pattern: a single access point to framework services inside a module. Through ModuleContext a module retrieves the current user, tenant, AI provider, payment service and i18n.
TenantContext
AsyncLocalStorage store for tenant context during a request. Guarantees data isolation between bots even under parallel requests in a single Node.js process.
CommandBus (BullMQ)
Command Pattern on top of BullMQ. Deferred jobs, retry with backoff, prioritization, cron scheduler. Separate workers offload the main process.
FSM Scenes
Finite state machine for multi-step user dialogs. State is stored in Redis between messages. Scenes are attached to modules with a decorator. Timeout and cancellation event support.
Need a Telegram bot or architecture consultation?
I design and launch Telegram products on my own framework. If you are interested in this stack or need help with bot architecture, write to me. Let's discuss the task without obligation.
I will respond within 24 hours.