Executive Summary
Telegram’s MTProto API is a low-level, stateful protocol that differs fundamentally from the Bot API. Unlike simple HTTP bots, an MTProto client maintains a long-lived, encrypted TCP (or WebSocket) connection with Telegram’s servers and must handle handshakes, session state, and update synchronization. Integrating an MTProto client into a TypeScript framework (on both traditional servers and serverless platforms) requires careful design of runtimes, storage, and networking. Key challenges include authorization (Diffie-Hellman key exchange and login), data-center (DC) routing and migration, managing update state (PTS/QTS/SEQ), and keeping connections alive (handling pings and reconnections). Existing JS/TS libraries (e.g. Teleproto/GramJS, @mtproto/core, mtcute) or C++-based TDLib (via wrappers) offer building blocks, each with trade‑offs in performance, maintenance, and ease of use.
This report analyzes the MTProto protocol components and available clients, proposes a parallel “Telegram Runtime” architecture within a generic API framework, and outlines a phased implementation roadmap (from MVP to production). It covers session storage models (file, database, encrypted), multi-account handling, worker/process isolation, inter-runtime communication, and serverless deployment patterns (e.g. AWS Lambda vs Cloud Run trade-offs). We also detail security (key management, encryption, permission), observability (metrics, logging), and testing strategies (mocking vs live updates).
Conclusions: Embedding a native MTProto client in a TypeScript framework is feasible but complex. A recommended approach is to define a generic runtime abstraction (e.g. app.runtime("telegram", options, handler)) that spawns a Telegram client session (possibly in a worker thread or container) alongside the HTTP runtime. Start with a minimal MVP (single-session, basic message handling), then add persistence, multi-session support, fault‑tolerance, and serverless-specific adaptations. Choice of library is critical: pure‑JS libraries (Teleproto, @mtproto/core, mtcute) are simpler to integrate but may lack high throughput; TDLib wrappers are faster but add native dependencies. Throughout, follow Telegram’s security guidelines and best practices for long-lived connections.
Background: Bot API vs MTProto (Telegram API)
Telegram offers two APIs: the Bot API (HTTPS-based, high-level, for bot accounts) and the Telegram (MTProto) API (low-level, encrypted, for user or bot accounts). The Bot API is easy to use (webhooks or polling) but limited to bots and has stricter rate limits. By contrast, MTProto is the “full” Telegram client protocol, supporting both user and bot accounts with access to features not exposed via the Bot API (deleted messages, higher limits, etc.). MTProto requires maintaining a continuous connection with Telegram servers, persisting session state, and implementing its RPC language and encryption. In practice, you cannot mix Bot API and MTProto on the same account simultaneously.
For a modern TypeScript framework, embedding an MTProto client means the server (or serverless function) behaves like a Telegram client. This allows responding to messages as a user or advanced bot, handling updates in real-time without webhooks, and overcoming some Bot API limits. However, it introduces complexity: statefulness, persistent connections, and cryptography. We must design the framework to run two “runtimes” in parallel (HTTP and Telegram) while fitting typical Node/TS environments (Node ≥12, possibly bundling) and serverless constraints (cold starts, timeouts).
Project Constraints & Design Goals
- Environment: TypeScript/JavaScript on Node.js (assuming Node 14+ for worker threads), with optional browser (less likely for server). Monorepo vs single-package not specified; assume a single TS package for simplicity. Code should be modular.
- Framework form-factor: Likely an Express/Fastify-like HTTP framework extended with plugin/runtimes. We assume a core
appobject to which we can attach runtimes (HTTP, Telegram, etc.). - Serverless Targets: Aim for compatibility with AWS Lambda, Cloud Run, Azure Functions, Vercel, etc. Each has limits on execution time, memory, concurrency, and background tasks. Special strategies (e.g. keep-alives, scheduled triggers, or using container-style services) will be needed.
- Lightweight & Efficient: The framework and Telegram client should use minimal resources. If ML model inference is needed, use small models (e.g. onnx.js or TensorFlow.js) and potentially isolate them.
- Security: Must follow Telegram’s Security Guidelines. Store API secrets (API_ID/hash, auth keys) securely (env vars, KMS), use TLS for external services, and ensure data at rest (sessions) is protected.
- Independent Threads/Runners: Design such that Telegram logic can run independently (e.g. in a worker thread or subprocess) without blocking HTTP. Provide an API so the developer can write
app.telegram(...)orapp.runtime("telegram", ...)alongsideapp.http(...). - Full HTTP/Telegram feature parity: The API should allow all necessary HTTP aspects (routing, middleware, security headers) and Telegram aspects (event listeners, sending/receiving messages) so developers have a uniform experience.
MTProto Protocol Overview
Three Layers (RPC, Cryptography, Transport)
MTProto is layered into RPC (TL language), Cryptographic (Auth Key & Encryption), and Transport. At the high level, clients issue Telegram API calls (methods like messages.sendMessage) over a session that is tied to an authorization key and client instance. Each session is identified by a 64-bit session ID (random per client) and holds state like pts, qts, and seq to synchronize updates. Multiple TCP/WebSocket connections can be open simultaneously, and responses may arrive on any connection of the same session.
Transport: MTProto can run over various transports: plain TCP/TLS, HTTP long-poll, WebSockets (WS/WSS), even UDP. In Node.js, one typically uses a raw TCP (TLS) connection for best performance. When using in-browser or platforms that disallow raw sockets, WebSocket (WSS) is an alternative. The official docs note MTProto can be layered on HTTP/WS if needed. Clients must implement heartbeats (ping/pong or ping_delay_disconnect) to keep TCP alive and detect disconnects.
Encryption: Each plaintext message (or container of messages) is encrypted with AES-256-IGE using a 256-bit key derived from a 2048-bit authorization key and a 128-bit message key. The 2048-bit auth_key is computed once via a Diffie–Hellman handshake when the client first runs (see “Creating an Authorization Key” below). Every message’s AES key/IV depends on: the auth_key, the message body (which includes session_id, msg_id, seqno, server_salt, etc.) and padding. Concretely, MTProto 2.0 prepends a 64-bit auth_key_id and a 128-bit msg_key to each packet, then encrypts the packet body. The msg_key is the middle 128 bits of SHA256(auth_key + plaintext), ensuring forward secrecy when combined with the auth_key. Important fields in plaintext before encryption include server_salt (64-bit, changes every 30 min), session_id (64-bit), msg_seqno (32-bit), and message payload. These prevent replay and tie messages to the correct session.
Authorization & Handshake: On first run (or if no saved key), the client performs a 4-step DH exchange (req_pq → resPQ → req_DH_params → dh_gen_ok) to create an auth_key. This is CPU-intensive (big-number factorization and RSA) and yields a unique key per user and device. The key is stored (often encrypted in session storage). After the auth_key is established, the client binds it to a “permanent” user account. For a user account, this means auth.sendCode → auth.signIn/auth.signUp. For a bot token, one can use auth.signInBot. (Note: TDLib often simplifies auth by handling QR codes or tokens automatically.)
Perfect Forward Secrecy (PFS): MTProto supports PFS by optionally creating a temporary auth key (auth.keygenTemp) that expires. The client can periodically generate a temp key (p_q_inner_data_temp_dc) and bind it via auth.bindTempAuthKey, so even if the permanent key is compromised, session eavesdroppers cannot decrypt past messages. This is advanced and mostly used in official clients; for most bots the permanent key approach suffices, but it's an available mechanism.
Session State, Sequences and Updates (PTS/QTS/SEQ)
A Telegram “session” is more than an open socket – it includes the stateful sync numbers needed for correct update handling. Key sequences:
seq(Update Sequence): A global counter of update “batches” (packets) delivered to this session. EachUpdatesorupdateShorthas aseq. If the client misses some, the server may sendnew_session_createdor requiregetState.pts(Points) andqts: “Points” track the event sequence in each “message box”. The common message box (private chats + basic groups) has its ownptscounter, incremented when messages in those contexts change. Each channel/supergroup has a separate message box with its ownpts. Secret chats and certain bot-related events use a secondary sequenceqts. EveryUpdatecarries the relevantptsandpts_count(number of events) so clients can detect missed updates.
A correct client must track these values. Telegram’s docs summarize that the client ensures integrity of: updates sequence (seq), common pts, secondary qts, and per-channel pts for group/channels. If a gap is detected (e.g. an expected pts is skipped), the client must call updates.getDifference to catch up. Well-designed libraries handle this state internally, but the framework should provide hooks (e.g. an “onUpdate” listener) and persistence to recover it across restarts.
Data Center (DC) Routing & Migration
Telegram has multiple global data centers (DC1–DC5 for users, plus others for media/CDN). The client must connect to the correct DC for the user’s account. A typical flow is:
- Find nearest DC: Call
help.getNearestDc(or use built-in defaults). The result is a suggested IP for the current region. - Auth/DC errors: If you attempt to send code or login on the wrong DC, the server responds with a migrate error: e.g.
PHONE_MIGRATE_4means “your phone is in DC4”. OnPHONE_MIGRATE_norNETWORK_MIGRATE_n, the client must switch the endpoint to DC n and retry the request. - Manual DC control: Good libraries parse these errors and transparently reconnect to the correct DC. The five DC addresses (as of writing) are known static IPs. For example, DC1=149.154.175.50:443, DC2=149.154.167.51:443, …, DC5=91.108.56.165:443. A library may maintain a DC registry (as the Elixir library [48] does) and rotate through them as needed.
Summary: The client code must watch for *_MIGRATE_ errors on methods like auth.sendCode or any request, then change to the specified DC and retry. Libraries like Teleproto and MTProto usually do this automatically (e.g. @mtproto/core “syncs auth on all DCs”).
Encryption Details (Summarized)
Per MTProto 2.0: before sending, a packet’s first bytes are auth_key_id (64b) and msg_key (128b). The AES key and IV are derived from the 2048-bit auth_key and the msg_key. Encryption uses AES-256-IGE. The plaintext of each message must begin with:
- server_salt (64-bit): random salt from server (changes ~30min).
- session_id (64-bit): random client-chosen ID for this session.
- msg_seqno (32-bit): incremental sequence (# of content messages sent ×2, +1 if reply).
- message length: etc.
These fields ensure freshness and uniqueness. The server salt especially thwarts replay; old salts remain valid for a short grace window.
For our framework, the library will handle this encryption. The key point is to persist the auth_key and server_salt securely (see session storage below) and to ensure client clocks stay roughly in sync (if your clock is off, server may reject msg_ids).
Transport and Connection Management
MTProto clients typically open a long-lived TCP/TLS connection to 149.154.167.50:443 (or other DC IPs). Best practice is to reuse a single socket for all calls (packages like Teleproto do this by default). Keep the connection alive by sending a ping at ~60s intervals or using ping_delay_disconnect to prevent idle drop. If the connection dies, the client should immediately reconnect and possibly re-send any un-acked messages. Most libraries abstract this: they catch low-level socket errors and reconnect transparently. We should ensure our runtime does not crash on a network error; instead, it should attempt exponential backoff reconnects. On reconnect, Telegram sends a new_session_created if it had to create a new server-side session (the client must ACK that).
Flood-wait / Rate-limit: Telegram may respond with error FLOOD_WAIT_X if too many calls are made. Libraries like GramJS throw a FloodWaitError with .seconds. By default, GramJS sleeps automatically for waits <60s. We should allow configuring this threshold or handling such errors (e.g. queue/delay calls).
In summary, the MTProto runtime must maintain a persistent, stateful connection (TLS socket) and gracefully recover from disconnections, DC migrations, and rate-limit signals. The framework should provide hooks/events for connection lifecycle (connected, disconnected, error) so the app can log or alert.
MTProto Client Libraries (Comparison)
Several implementations exist. We compare the main ones relevant to a TypeScript/JS project:
| Library | Language & Env | Native deps? | TL Layer | License | Status & Maturity | Notes/Pros & Cons |
|---|---|---|---|---|---|---|
| Teleproto (GramJS fork) | TypeScript/JS (Node, Browser) | No (pure JS) | 158 (latest) | MIT | Active fork of archived GramJS; ~500★ | Easy to use; multi-DC sync; no native builds. Lighter than TDLib. However, GramJS itself is archived, and teleproto is newer and less battle-tested than older libs. Good TS typings. |
| @mtproto/core | JavaScript (Node/browser) | No (uses Node crypto & net/ws) | 158 (latest) | MIT | Actively maintained (Ali Gasymov). ~1.5K★ | Modern API, automatic DC sync (claims “sync auth on all DCs”). Node uses TCP, browser uses WebSocket. Good error handling. Slightly lower-level (you call mtproto.call(...)). Storage plugin model. |
| mtcute | TypeScript (Node, Deno, Bun, Browser) | No | Latest (0.31.0) | MIT | Active (~500★, 120+ releases) | Modern, low-level MTProto core with “Dispatcher” for handlers. Emphasizes small memory (~50MB). Truly up-to-date (v0.31, Jul 2026) and multi-runtime (Node, Deno, Bun). Still maturing. |
| TDLib via tdl/tdlib-native | C++ with TS wrapper (Node.js) | Yes (native addon) | N/A (uses TDLib schema 182+) | MIT | Active (TDLib 1.8.x); wrappers (~400★) | Very fast, full feature, built-in caching. Good for heavy-duty (multi-dc, large volumes). However requires native builds (multi-platform issues) and ~5-10MB binary. Overkill for simple bots. Best throughput. |
| telegram-mtproto (souche) | JavaScript (Node/browser) | No | 147 (older) | MIT (originally) | Obsolete (no longer maintained) | Early JS client by souche. Supports multi-devices but outdated. Not recommended. |
| Other languages (for context): Telethon (Python/MIT), Pyrogram (Python/GPL), MadelineProto (PHP/GPL) – mention only as background; they’re not directly usable in a TS project but demonstrate alternative designs. |
Serverless Suitability: Pure-JS libraries (Teleproto, @mtproto, mtcute) run on Node or bundlers and can be deployed to container-like serverless (Cloud Run, AWS ECS). They do not natively run on strict FaaS with <15min execution (Lambda, Vercel) because they need always-on connections. TDLib (via tdl) similarly needs a running process. For truly ephemeral functions, one might only do a quick connect+logout sequence for specific tasks, or use scheduled jobs (cron) to simulate polling. In practice, a container model (Cloud Run, EC2, Fargate) is a better fit for MTProto.
Library Selection Summary: For a TS framework, a pure-TS library is simpler to integrate (no native build). Teleproto or mtcute are natural choices. Teleproto is mature (fork of Telethon-derived code) and documented; mtcute is newer and very modern/typed. @mtproto/core is another strong contender with broad usage. For maximum performance, TDLib-based solutions are options, but they add complexity.
Sources: Library docs and repos.
Persistent Connections & Reconnect Strategies
Since MTProto requires a continuous TCP connection, we must design the runtime to stay alive or recover quickly. Key practices:
Keep-alives: Send periodic
pingorping_delay_disconnect(Telegram supports up to ~60s by default) to avoid timeouts. Many clients handle this automatically. When apongornew_session_createdis received, clear any reconnect timers.Reconnection logic: If the socket drops (network glitch, server reboots, etc.), immediately attempt reconnect (possibly with backoff) to the same DC and session. Upon reconnect, if the server indicates the session was lost (via
new_session_createdor missing packets), you may need to re-authorize or requestupdates.getDifference. Libraries should encapsulate this. Ensure at-most-once semantics by tracking unacknowledged message IDs and re-sending if needed.Handling rate limits (FLOOD_WAIT): When an RPC returns a
FLOOD_WAIT_Xerror, pause further requests forXseconds. E.g. GramJS auto-sleeps for small waits. Our framework should allow configuring a global flood-sleep threshold or catching these exceptions in user code (the library usually throws a specific error class).Queues and backpressure: If many parts of the app want to call the Telegram API concurrently, use an internal queue to throttle requests through the single connection. Most MTProto libraries provide a single
clientobject whose methods are naturally queued on the connection.
Example: Ping/Pong Handling (Mermaid)
Below is a simplified sequence of ping handling. The client sends ping_delay_disconnect, the server responds with pong and may schedule a disconnect timer. The client must reset the timer with periodic pings (every ~60s).
sequenceDiagram
Client->>Telegram: ping_delay_disconnect (ping_id, delay=90s)
Telegram-->>Client: pong (msg_id, ping_id)
Note right of Telegram: Server will disconnect after 90s<br>unless another ping is received
Client->>Client: (after 60s) send next pingThis ensures the connection stays open indefinitely. If the client fails to ping, Telegram will close the TCP connection (which the client should detect and reconnect).
Session Persistence (Storage Models)
MTProto clients must persist session state across restarts. Key data to store per “user session”:
- auth_key (with its ID) – the main 2048-bit key (or its first 64-bit ID).
- server_salt – latest salt from server. (Important to update periodically.)
- session_id – the random 64-bit session identifier (constant per client instance).
- last msg_seqno & msg_id – to resume monotonically increasing IDs.
- updates state (pts, qts, seq) – so we don’t miss past updates.
- User info – (api_id, api_hash, maybe phone or username) to re-init.
The session storage can be a simple JSON string or binary blob, or a database record. For example, a database schema might be:
// Example TypeScript interface for a Telegram session record:
interface TelegramSession {
id: string; // unique session ID (e.g. UUID or DB primary key)
accountTag: string; // developer-defined tag (e.g. user ID or bot name)
apiId: number; // Telegram application API_ID
apiHash: string; // Telegram application API_HASH
authKey: Buffer; // binary auth_key (encrypted at rest)
authKeyId: bigint; // 64-bit key ID for quick lookup
serverSalt: Buffer; // 64-bit server salt
sessionId: bigint; // 64-bit session identifier
lastSeqNo: number; // last message sequence number
pts: number; // last pts value
qts: number; // last qts (for secret chats/bots)
createdAt: Date;
updatedAt: Date;
}In a relational DB, you might store authKey, serverSalt as VARBINARY(8) or in a secure Vault. In a key-value store (Redis, DynamoDB), you could store the entire JSON. For example, authKey is sensitive and could be encrypted with a master key if needed.
Most JS libraries offer session files out of the box: GramJS’s StringSession saves a base64 string, and StoreSession("folder") keeps JSON files. @mtproto/core uses a JSON file storage by default (see [25] where storageOptions.path points to a JSON file that stores auth_key_id, salts, timeOffset, etc.). For serverless, ephemeral file systems mean you’d use an external store: e.g. a database or object storage (S3). So the framework should allow pluggable storage backends (file, memory, DB, cloud storage).
Mermaid: Session Data Flow
flowchart LR
A[Client Init] --> B{Has saved session?}
B -- No --> C[Run key exchange → new auth_key, session_id]
C --> D[Save new auth_key, salt=0, session_id]
B -- Yes → E[Load auth_key, salt, session_id from storage]
D --> F[Authenticate user/bot (sendCode, signIn)]
E --> F
F --> G[Start listening for updates, store incoming states]
G --> H[On update: update pts/seq, handle events]
H --> I[Periodically save session (salt, seqno, pts, qts) to DB/file]This flow ensures that once a user logs in, the session is saved, and on subsequent restarts we reload the exact state to continue seamlessly.
Multi-Account & Multi-Session Architecture
A single framework instance may need to support multiple Telegram sessions (e.g. multiple user accounts or bots). Design considerations:
Isolation: Each account/session should have its own auth_key, salts, connection. You can implement this by instantiating separate client objects. In code:
const client1 = new TelegramClient(session1, ...); const client2 = new TelegramClient(session2, ...);. Each runs on its own TCP socket or, optionally, its own Worker thread.Indexing: Store sessions in a map (keyed by account ID, phone number, or database ID). On startup, load all session configs from DB and create clients for each. Provide a way for the app to select the right client (e.g.
app.telegram.useSession(userId)).Concurrency: If many sessions are expected, consider using Node’s Worker Threads or clustering. Each session can run in its own thread with message-passing, avoiding blocking the main event loop. Alternatively, lightweight sessions can run in the same thread if traffic is low.
Resource Caps: Each session consumes memory (storing caches) and a socket. Monitor and limit how many you create simultaneously.
Event Dispatch / Plugin API Design
The framework should expose Telegram events similarly to HTTP routes. For example, an API like:
app.runtime("telegram", { apiId, apiHash, session: sessionString }, (client, dispatcher) => {
dispatcher.on("message", async (ctx) => {
console.log("Received message:", ctx.text);
await client.sendMessage(ctx.chatId, { message: "Echo: " + ctx.text });
});
});Here app.runtime("telegram", options, handler) registers a Telegram runtime. Inside, the developer gets a client object (from Teleproto/mtproto) and possibly an event dispatcher. We could model this after Telethon’s or Teleproto’s dispatcher: e.g. a Dispatcher that wraps the raw client and provides filters (see [40†L330-L339]). The framework might also allow middleware (authentication, logging) on Telegram events.
Design the TypeScript API surface to be ergonomic: e.g.
app.telegram({ apiId, apiHash, sessionPath: "./session.json" })
.onUpdate("message", async (msg) => { ... });Or using a router-like syntax. The exact design is speculative, but should hide the low-level MTProto calls.
Worker/Isolation Models (Threads vs Processes)
Node.js is single-threaded by default. For heavy or blocking tasks (like crypto key generation or ML inference), using worker threads or child processes is advisable.
Worker Threads: Since Node 12+,
worker_threadsallow running JS code in parallel. We could run each Telegram session in its own Worker (with separate V8 isolate), communicating viapostMessage. This isolates state and allows multi-core usage. However, passing the TelegramClient across threads is complex (no direct sharing), so we’d re-create the client inside the worker. Alternatively, spawn a generic worker template that loads a session and communicates events.Child Processes: Using
child_process.fork()to spawn separate Node processes for each session or for heavy computation. Communicate via IPC (process.send). Similar trade-offs as threads, but heavier weight.Native Addons: If using TDLib, it spawns its own threads internally. We must be careful not to block the Node event loop when calling its methods. The
tdllibrary andtdlib-nativewrapper are async/Promise-based, but under the hood use libuv threads.Web Workers (Edge): In browser or Deno environments, one would use Web Workers. Not directly relevant to a Node server.
Recommendation: Start simple: run all Telegram clients in the main thread asynchronously. If performance shows issues, refactor to use Worker Threads per session. Ensure the framework abstracts this so user code (registered handlers) doesn’t need to worry about thread boundaries. For example, messages coming from thread can be re-emitted in the main event loop.
Inter-Runtime Communication (IPC, Messaging)
If HTTP and Telegram runtimes need to talk, or if multiple threads exist, choose a communication pattern:
In-Process Event Bus: If everything is in one Node process, you can use an internal event emitter or pub/sub. For example,
app.emit("telegramMessage", msg)that HTTP handlers listen to.Message Queues: For cross-process or cross-service comm, use Redis Pub/Sub or a message broker (RabbitMQ, Kafka). The Telegram runtime publishes events, HTTP runtime subscribes.
Shared Storage: Put updates or tasks in a shared DB or KV store. For example, Telegram runtime writes incoming messages to a database which HTTP routes poll (simpler but introduces latency).
A design example: The Telegram runtime could, on receiving a command, enqueue a job for the HTTP part. E.g. client.on("message", async ctx => { await jobQueue.add("processTelegramCmd", { ...ctx }); });. The HTTP logic reads from that queue.
Serverless Deployment Patterns
As noted, typical serverless functions cannot hold a long TCP connection indefinitely. Some patterns:
AWS Lambda / Google Cloud Functions / Azure Functions (CRON triggered): Set up a scheduled trigger (e.g. CloudWatch Event every minute). Each run spins up, connects to Telegram, calls
getUpdates-style queries (using Telegram’supdates.getDifferenceor MTProtoupdates.getState/updates.getDifference) and immediately exits. This is tricky: MTProto is not designed for short connect/disconnect loops. You’d lose session continuity unless you persist it (which you can), but you’d also hit auth limits. Generally not recommended for real-time chat.Cloud Run / ECS / Containers: Package the entire app as a container. Cloud Run supports background threads (the container stays alive as long as traffic flows). You can have a warm container running the MTProto client indefinitely. Use a single container instance (min scale=1) to keep it alive. Cloud Run can run on HTTP trigger or forever (via internal HTTP echo). For AWS, consider ECS/Fargate with autoscaling (keep at least 1 task running).
AWS Lambda (short run) + SNS + Step Functions: Advanced: Use Lambda in conjunction with AWS Step Functions (e.g. a state machine that keeps triggering itself), or use [AWS Lambda Function URLs + WebSockets + DynamoDB Streams] to mimic a service. This is very complex and unusual.
Edge Workers (Cloudflare): Not possible – Cloudflare Workers have no raw TCP sockets, only outbound HTTP/WebSockets. Even with WebSockets, Workers time out after ~120s.
Given the complexity, cloud containers or VMs are more suitable. That said, if one really wants FaaS, the framework could allow “stateless call” mode: run a client method once and exit. E.g. app.telegram.once('state', handler) style – but this defeats the purpose of receiving updates.
Operational Notes (Runbook):
- AWS Lambda: Keep connections alive by reusing container between calls (Lambda reuse environment). Use Node’s HTTP keep-alive agent for outbound calls. But remember Lambda may freeze/kill container. Best to use short timeouts.
- Cloud Run: Use a healthcheck (e.g.
GET /health) so Google knows service is live. Expose a small HTTP server even if mainly using Telegram. Log to Cloud Logging. SetminInstances=1to avoid cold starts. - Vercel: Not suitable for long polls or persistent connections. Only use for stateless endpoints.
- Azure Functions: If using App Service plan with Always On, can mimic container. But consumption plan will teardown idle.
- Secrets: Use environment variables or secret managers (AWS Secrets Manager, GCP Secret Manager) for API_ID, API_HASH.
- Time-sync: Ensure NTP on the host because Telegram rejects messages >300s off.
In essence, plan to deploy as a long-lived service (e.g. Cloud Run, Kubernetes, EC2 Container), not a short-lived function. For serverless light use (e.g. quick broadcast), see if one can use Telegram Serverless API or Bot API as a fallback.
Security Considerations
- Authentication & Secrets: Store
api_hash,auth_keyand session strings encrypted (e.g. using a KMS). Do not log them. Provide a permission model: e.g. restrict who can create new Telegram sessions. - Encryption at Rest: If storing sessions in a DB, encrypt the auth_key. If file-based, protect the directory.
- Transport Security: Always use TLS for anything (Telegram is TLS by default). For custom transports, use secure channels.
- Rate Limiting: Although MTProto bypasses Bot API limits, respect user privacy. Implement your own rate-limits on sending (e.g. to avoid being banned).
- Least Privilege: The Telegram client runs as the same identity as the server. Ensure the hosting environment limits its access (don’t run as root, restrict file permissions).
- Compliance: Follow Telegram’s API Terms. For example, do not misuse user accounts (spam, scraping). If using user accounts, ensure you have consent.
- Security Guidelines: Telegram’s own client-security guidelines should be followed (no logging plaintext chats, no MITM, etc.). Keep libraries up to date (MTProto protocol evolves).
Resource & Scaling Considerations
- Memory: Each Telegram client holds caches and buffers (GramJS clients ~30-50MB as claimed). Budget accordingly.
- Concurrency: Node’s async model handles I/O well, but CPU-heavy tasks (DH handshake, crypto, ML inference) should be offloaded to workers.
- Horizontal Scaling: If needed, scale by running multiple framework instances (each with their own sessions). For example, shard based on account ID. Use a shared DB for session store so any instance can pick up any session.
- ML Models (if used): If running lightweight ML (e.g. TensorFlow.js for NLP), load them lazily, cache models in memory, and consider using GPU instances if performance is needed. On serverless, CPU is limited so keep models very small or use cloud inference.
Observability (Metrics, Logging)
- Instrument key metrics: connection uptime, messages received/sent per minute, flood-waits, reconnection count, failed logins.
- Use logs structured by session (prefix with session ID).
- Trace RPC calls: measure latencies of key methods.
- Expose a
/metricsendpoint (Prometheus) or integrate with Cloud Monitoring. - Handle errors robustly: log stack traces of unexpected errors, but sanitize messages (no secrets).
- Health checks: monitor if the Telegram socket is alive (e.g. a periodic function that pings Telegram and verifies answer).
Testing Strategies
- Unit Tests: Mock the Telegram client interface. For example, use a fake
TelegramClientthat emitsmessageevents. Test your handler logic in isolation. - Integration Tests: Use a real Telegram test account (or Bot token) to send test messages. The mtproto-core docs suggest using test phone numbers or ephemeral accounts for CI. You can script sending a message from a test account and assert your framework catches it.
- E2E Tests: In CI/CD, optionally run an actual session for a short time to ensure your connection logic works (maybe on a non-production environment).
- Load Testing: Simulate many messages (e.g. with a script using gramjs or telethon) to measure throughput and memory. Test flood wait behavior (e.g. spam rapid messages to trigger FLOOD_WAIT errors and confirm correct handling).
- Security Testing: Penetration test the secrets management, verify no sensitive data leaks into logs.
Legal/Terms of Service Considerations
- Using MTProto to automate a user account is allowed by Telegram’s terms if it’s your account and you’re not doing abuse. (Automating other users’ accounts without consent would violate terms.)
- If using bot accounts with MTProto, note you cannot simultaneously use the Bot API on the same bot token; treat it as a user (see [28]).
- Do not use MTProto to artificially inflate stats or violate the Terms. Always abide by the usage rules (no spamming, etc.).
Performance Benchmarking Approach
- Compare initialization time (auth) and per-request latency for chosen libraries. GramJS-style libs are pure JS crypto (slower than TDLib’s C++). For critical paths (e.g. sending many messages), benchmark throughput.
- Measure memory footprint of each session.
- Benchmark on target environments: e.g. AWS Lambda warm vs cold, Cloud Run concurrency.
Exact numbers will vary by hardware; the key is to ensure the implementation meets real-time requirements (e.g. replying to messages within 1-2 seconds). If not, consider moving to a faster library (TDLib) or optimizing.
Comparative Table of Libraries
| Library | Language/Env | Native | TL Layer | License | Maturity & Repo | Key Notes |
|---|---|---|---|---|---|---|
| Teleproto (GramJS fork) | TypeScript/JS (Node, Browser) | No | 158 | MIT | Archived GramJS → Teleproto fork. ~556 commits; 11k★ (GramJS) | ⚠️ Archived: GramJS is archived. Teleproto (actively maintained fork, ~526★) largely compatible. Multi-DC, browser support. No native deps. High-level client/dispatcher API. |
| @mtproto/core | JS (Node/browser) | No | 158 | MIT | ~500★; actively updated (Ali Gasymov) | Modern, high-level .call(method). Automatic DC syncing. Good error classes. Built-in storage (JSON file). Good TypeScript support. |
| mtcute | TypeScript (Node, Deno, Bun, Browser) | No | 164 | MIT | ~526★, 124 releases (up-to-date) | Very modern design with Dispatcher API. Claims <50MB footprint. Multi-runtime (Deno/Bun). Active development (v0.31.0 as of Jul 2026). Relatively new but promising. |
| tdlib-native (AlexGrib) | C++ (via Node addon) | Yes | N/A (TDLib 1.8+) | MIT | ~400★; active (uses latest TDLib 1.8.37) | Wrapper around official TDLib (blazing-fast). Built-in DB caching, updates, multi-device. Native dependency (~4-20MB). Complex deployment (binaries). Only supports Node, not browser. Ideal for high-performance needs. |
| telegram-mtproto (souche) | JS (Node, Browser) | No | 149 | MIT (old) | Abandoned (last commit ~2018) | Early MTProto lib by souche. Single API instance, event-driven. Outdated (Layer 149). Not recommended for new projects. |
| telethon (for context) | Python | No | ~164 | MIT | Mature (One of first MTProto libs). Very well tested. | Mentioned for conceptual reference. Not usable in TS directly. |
| pyrogram (context) | Python (GPL) | No | 157 | GPL | Active, very popular for Python. | GPL (incompatible for closed-source). For reference only. |
| madelineProto (PHP) | PHP | No | ~ (shadows TL) | GPL | Active PHP client (used in many bots). | GPL, PHP only. |
(“Layer” refers to Telegram API TL layer version; higher is newer. Serverless column omitted since all need a long-running process.)
Proposed Architecture & Diagrams
Architecture Overview
graph LR
subgraph App (Node.js Process)
A[HTTP Runtime]
B[Telegram Runtime]
C[Worker Threads]
A -->|HTTP Requests| FrameworkCore
B -->|MTProto RPC| TelegramServers[Telegram DCs]
A -->|Invoke| B
A -->|IPC/pubsub| C
C -->|Results| A
end- App: Our TypeScript server.
- HTTP Runtime: Handles incoming HTTP API calls or webhook requests.
- Telegram Runtime: Manages MTProto connections. Could be in the main thread or a Worker. It uses an MTProto client library to connect to Telegram DCs.
- Worker Threads (optional): For heavy tasks (e.g. ML inference, media processing), with communication back to main app (message bus or RPC).
- Telegram DCs: Telegram’s servers (multiple data centers).
This shows HTTP and Telegram runtime as peers under the same process, communicating as needed (via event emitters or shared state).
Startup & Login Sequence
sequenceDiagram
participant App as Framework
participant Telegram as MTProtoClient
participant Server as Telegram DC
App->>Telegram: initialize client(options)
Telegram->>Server: req_pq (nonce)
Server-->>Telegram: resPQ (pq, server_nonce, fingerprints)
Telegram->>Telegram: factor pq -> p,q
Telegram->>Server: req_DH_params (p,q, nonces)
Server-->>Telegram: server_DH_params (encrypted_nonce)
Telegram->>Telegram: decrypt, gen tmp_key
Telegram->>Server: set_client_DH_params (encrypted_data)
Server-->>Telegram: dh_gen_ok (nonce, server_nonce, new_nonce_hash)
Telegram->>Telegram: compute auth_key, save it (once)
Telegram->>Server: auth.sendCode (phone_number)
Server-->>Telegram: auth.sentCode (phone_code_hash)
Telegram->>Server: auth.signIn (phone, hash, code)
Server-->>Telegram: auth.authorization (session created)
Telegram->>App: "Logged in"This diagrams the initial key exchange and login (for a user account). After auth.signIn, the client receives auth.authorization with user info. The client then transitions to “ready” state. Error conditions (e.g. PHONE_MIGRATE_X) would loop back to adjust the DC.
DC Migration Example
sequenceDiagram
participant Client
participant Server
Client->>Server: auth.sendCode(req to default DC)
Server-->>Client: PHONE_MIGRATE_4
Client->>Client: (parse error) update to DC4, reconnect
Client->>Server: auth.sendCode(to DC4)
Server-->>Client: auth.sentCodeUpon receiving PHONE_MIGRATE_4, the client re-initiates connection to the indicated DC (4) and retries the request. Modern libraries handle this automatically.
Message Flow (Runtime Perspective)
sequenceDiagram
participant HTTP
participant Telegram as TGClient
participant Server as Telegram DC
HTTP->>Telegram: app.startTelegramRuntime(...)
Telegram->>Server: connect/auth
Note over Server: Client logged in, ready
Server-->Telegram: push updates (msgs)
Telegram->>Telegram: emit incoming message event
Telegram->>HTTP: deliver event (via event bus)
HTTP->>HTTP: business logic (e.g. enqueue ML task)
HTTP->>Telegram: sendMessage (reply)
Telegram->>Server: messages.sendMessage RPC
Server-->>Telegram: updates (msg sent)This shows the continuous loop: Telegram client receives updates and passes them to HTTP-layer handlers; those handlers may in turn call Telegram RPCs to respond.
Implementation Roadmap (MVP → Production)
Prototype MVP:
- Choose library: e.g. install Teleproto (
npm i teleproto) or@mtproto/core. - Single-session proof of concept: Write a minimal script that logs into a Telegram account (could use a throwaway test account or Bot token) and logs incoming messages to console. Save the session (e.g.
client.session.save()in GramJS). Test sending a message. - Integrate into framework: Expose this as
app.telegram(...)or similar. Provide a config forapiId,apiHash, andsessionFile. Test thatappcan route HTTP requests and Telegram events concurrently. - Connection resilience: Ensure on first connect and on disconnect, the app does not crash. Add logging for connect/disconnect. Handle one
PHONE_MIGRATE_Xto verify DC switching.
- Choose library: e.g. install Teleproto (
Basic Features:
- Event API: Implement an event dispatcher so developers can write handlers (onMessage, onCallback, etc.) in the framework. Normalize data to TS-friendly objects.
- Session persistence: Use a file or DB. Write TypeScript interfaces (like above) and code to save/load sessions. Ensure threads/processes can access it.
- Flood and errors: Wrap RPC calls with try/catch. Use the library’s error classes. For FLOOD_WAIT, auto-sleep or bubble error to handler. Document it.
- Concurrency control: If multiple incoming updates, queue calls to Telegram to avoid race conditions.
Multi-Session & Workers:
- Support multiple accounts: Allow
app.telegramto be called multiple times with different session paths or IDs. Internally manage a map. - Isolation (optional): Experiment with running each client in a
new Workerthread. If so, define an IPC protocol (e.g. parent triggers worker with init params; worker sends updates back). - Inter-runtime comm: If workers, use
parentPortmessages; otherwise, use an in-process event bus.
- Support multiple accounts: Allow
Serverless Adaptations:
- Deployment targets: Create Dockerfile for Cloud Run/AWS ECS. Document Lambda support issues.
- Keep-alive strategies: For Cloud Run (with HTTP server), use a background event loop to ping Telegram. For Lambda, consider a short-lived approach (likely skip real-time).
- Infrastructure as code: Write examples (Terraform/Serverless Framework) to deploy on AWS/Azure/GCP, setting proper timeout, env vars, etc.
Security & Testing:
- Secrets management: Integrate with environment or secret store (AWS Secrets Manager for credentials). Never hard-code.
- TLS pinning (optional): Telegram’s public keys could be pinned or checked (advanced).
- Unit tests: Mock the Telegram client; write tests for handlers.
- Integration tests: (Optional) Spin up a real Telegram account for CI (could use a Telegram bot token here even though using MTProto).
- Documentation: Provide clear docs for
app.telegramusage and session setup.
Observability & Hardening:
- Add logging of all incoming/outgoing updates (at least debug level).
- Expose metrics endpoint.
- Failover logic: e.g. if login fails thrice, send an alert.
- Stress test with simulated load; optimize memory leaks or slow paths.
Data Models for Session Storage
Below is an example JSON representation of a saved session (using Teleproto’s StringSession concept):
{
"auth_key": "BASE64_ENCODED_256_BYTES",
"auth_key_id": 1234567890123456789,
"server_salt": 9876543210987654321,
"session_id": 1827364518273645,
"last_msg_seqno": 42,
"last_msg_id": "1627384950293849501",
"pts": 100,
"qts": 50,
"time_offset": 0
}A corresponding Prisma or TypeORM schema might define these fields in a table. For example, a SQL table:
CREATE TABLE telegram_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_name TEXT UNIQUE, -- e.g. "user123" or bot name
api_id INT NOT NULL,
api_hash TEXT NOT NULL,
auth_key_id BIGINT NOT NULL,
auth_key BYTEA NOT NULL,
server_salt BIGINT NOT NULL,
session_id BIGINT NOT NULL,
last_msg_seqno INT NOT NULL,
last_msg_id BIGINT NOT NULL,
pts INT NOT NULL,
qts INT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);(Protect auth_key with column encryption or application-level encryption.) The app would update this record after login (to store auth_key, session_id) and after each session change (new salt, new seqno).
API/SDK Surface Design (TypeScript Examples)
Here’s a sketch of how the framework API might look to a developer:
import { App } from "myServerFramework";
// Create app instance
const app = new App();
// HTTP routes
app.http.get("/health", (req, res) => res.send("OK"));
// Telegram runtime setup
app.runtime("telegram", {
name: "mainBot",
apiId: 12345,
apiHash: "abcdef123456...",
sessionPath: "./sessions/mainBot.json",
connectionRetries: 3
}, (client, dispatcher) => {
// client is the MTProto client instance
// dispatcher is an event emitter for updates
dispatcher.on("message", async (ctx) => {
console.log(`Message from ${ctx.sender.username}: ${ctx.text}`);
// Echo message
await client.invoke(new Api.messages.SendMessage({
peer: ctx.chat, message: "Echo: " + ctx.text
}));
});
});
// Another Telegram runtime (optional)
app.runtime("telegram", {
name: "secondAccount",
apiId: 54321,
apiHash: "fedcba654321...",
sessionPath: "./sessions/second.json"
}, (client, dispatcher) => {
// ...
});
// Start the app (HTTP server + Telegram connections)
app.start({ port: 8080 });This illustrates an app.runtime(type, options, callback) design. Alternatively, one might use app.telegram(...) shorthand. The key is that inside the callback, the developer has a Telegram client object and event stream to handle updates. The framework merges or namespaced these runtimes internally.
Security Checklist & Operational Runbook
- Credentials: Store
apiHash, bot tokens, etc. in env vars or secret manager. Rotate keys if compromised. - Permissions: Run as non-root. Restrict network egress if only Telegram needed.
- Session Encryption: If saving sessions to disk or DB, encrypt the auth_key (e.g. use libsodium to encrypt with a master key stored securely).
- Logging: Do not log auth data, only IDs. Sanitize user content if logging (to avoid PII leaks).
- Monitoring: Deploy health checks (HTTP endpoints) and integrate with monitoring. Alert on disconnects or repeated login failures.
- Backups: If using persistent DB for sessions, backup regularly. If file-based, ensure volume persistence in container.
- Scaling: For Cloud Run/AWS, configure concurrency/replica counts. Use horizontal scaling carefully (sessions shouldn’t be duplicated across instances; use sticky assignments if needed).
- Cloud Specifics:
- AWS Lambda: Use VPC if needed; mind the 15-min limit. Use an ALB or Function URL for health check.
- Cloud Run: Set container CPU > 1 if doing heavy async work. Reserve memory (>=512MB) for safety.
- Vercel/Azure: Likely not feasible for MTProto; mention as not recommended.
Refer to each provider’s docs for CI/CD. E.g. AWS best practices recommends reusing connections via keep-alive (we’ll use Node’s HTTP agent accordingly).
Recommended Libraries, Tools & Code Snippets
- MTProto Library: Start with @mtproto/core or teleproto. Both hide cryptography.
- Dispatcher: Use gramjs/teleproto’s Dispatcher or mtcute's Dispatcher to ease event handling.
- HTTP Framework: Could be Express, Fastify, or a custom framework (since the question implies building one).
- Storage: For TS projects, an ORM like Prisma or TypeORM can model session tables. For local dev, JSON or SQLite files are okay.
- Testing: Use jest or mocha. For integration,
gramjsortelethon(Python) as a test client to send messages. - Containers: Use Docker, Node 18 image, install dependencies, set
CMD ["node", "dist/index.js"]. - CI/CD: Automate deploying to AWS (Lambda + ECR for container, or EKS/ECS), Google Cloud Run, or Azure.
Sample code snippet (using @mtproto/core):
import MTProto from '@mtproto/core';
const mtproto = new MTProto({
api_id: API_ID, api_hash: API_HASH,
storageOptions: { path: './data/session.json' }
});
async function start() {
// phone, code, password callbacks...
await mtproto.call('auth.signIn', { /* params */ });
console.log("Logged in", await mtproto.call('users.getFullUser', { id: 'me' }));
// listen for updates (mtproto uses manual polling here as example)
mtproto.updates.on('updates', (updates) => {
for (let update of updates) {
console.log("New Update:", update);
}
});
}Citations: Official docs and implementations informed these recommendations.