METABYTE
Back to articles

Telegram Mini App Backend Architecture in 2026

A pragmatic, production-ready backend architecture for Telegram Mini Apps in 2026: auth, payments, scaling, trade-offs, and what actually breaks under load.

8 mai 202614 min readAI research draft
Telegram Mini App Backend Architecture in 2026

If you get Telegram Mini App backend architecture wrong, you pay for it in fraud, failed payments, and weeks of debugging sessions that only reproduce on iOS at 2 a.m. This guide is the blueprint we wish more teams had before launch.

You’re here for a concrete 2026-ready reference: how to verify Telegram initData, issue short-lived tokens, structure services, pick the right database/queue, handle Stars/Payments, and scale without painting yourself into a corner. Here’s the short answer: verify initData server-side, mint a 5–15 minute JWT, keep a clean API boundary, use Postgres + Redis + a lightweight queue, cache aggressively at the edge, and treat Telegram webhooks as at-least-once events with idempotency.

What your Mini App backend must do

A Telegram Mini App is “just a web app,” but your backend does more than serve JSON. The reliable ones cover these jobs:

  • Authentication: Verify Telegram initData on every session start. Mint a short-lived server token; do not treat client initData as sufficient for all subsequent calls.
  • User model: Map Telegram users (id, username, language_code) to your own users table. Handle migrations if a user’s Telegram profile changes.
  • Payments: Support Telegram Stars and (optionally) provider-based payments via invoices. Implement idempotency and ledger entries you can audit.
  • Game/app state: Inventories, progress, cooldowns, streaks, leaderboards. Model them sanely, index for reads, batch writes.
  • Notifications: Bot messages, receipts, daily rewards. Throttle to avoid rate limits.
  • Anti-abuse: Replay protection, device fingerprints where ethical, server-side validation of game actions.
  • Observability: Structured logs with telegram_user_id, traces, SLOs for p95 latency. If something breaks in prod and you can’t see it, it will keep breaking.

Reference architecture for 2026

A production-grade setup we recommend for most Mini Apps (50k–1M MAU) looks like this:

  • Edge: Cloudflare CDN + WAF + Turnstile (optional for non-chat entry points), HTTP/2/3, caching for static assets.
  • API Gateway: Simple rate limiting and auth middleware. Can be a lightweight reverse proxy (NGINX, Envoy) or your framework’s edge runtime.
  • App API Service: Go (high throughput) or Node/TypeScript (ecosystem speed). Exposes REST/JSON or tRPC.
  • Postgres: Primary data store. Use read-replicas if you’re heavy on leaderboards or analytics reads.
  • Redis: Caching (sessions, profiles, feature flags), rate limiting, idempotency keys.
  • Queue/Workers: BullMQ (Redis), or NATS JetStream for higher scale. Offload heavy or retriable tasks.
  • Storage: S3-compatible (R2, B2) for assets/screenshots.
  • Webhook Ingestor: Dedicated endpoint for Telegram Bot API updates (invoices, callbacks). Writes compact, idempotent events to a queue.
  • Observability: OpenTelemetry traces, Prometheus metrics, Loki/Lake for logs. Alerts on error budget burn and Telegram API failures.

Flow (happy-path):

  1. User opens Mini App -> Telegram injects initData.
  2. Client sends initData to /auth/telegram.
  3. Backend verifies HMAC, mints short-lived JWT (5–15 min), returns profile snapshot + feature flags.
  4. Client uses JWT in Authorization: Bearer … for subsequent API calls.
  5. Payments: client requests an invoice (Stars or provider). Server creates invoice via Bot API, stores pending purchase, and awaits webhook confirmation to finalize.
  6. Worker processes webhooks/events, updates ledgers/inventory, emits bot messages as needed.

If you anticipate >2k RPS burstiness from launches, insert a lightweight in-memory gate (e.g., a token bucket in edge workers) to smooth spikes and protect Postgres.

Verifying initData and issuing server tokens (TypeScript)

Telegram’s initData is a URL-encoded set of key/value pairs plus hash, signed with a key derived from your bot token. Per Telegram’s Mini App docs, you compute data_check_string by sorting keys (excluding hash) and joining as key=value\n. Then:

  • Derive secret_key = HMAC_SHA256("WebAppData", bot_token)
  • Compute HMAC_SHA256(data_check_string, secret_key) and compare with hash

Below is an Express-style handler that verifies initData and mints a short-lived JWT. It also demonstrates a basic Redis idempotency guard for auth bursts.

import express from 'express';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import Redis from 'ioredis';

const app = express();
app.use(express.json());

const BOT_TOKEN = process.env.BOT_TOKEN!; // keep this secret
const JWT_SECRET = process.env.JWT_SECRET!;
const redis = new Redis(process.env.REDIS_URL!);

function buildDataCheckString(params: URLSearchParams): string {
  const entries = Array.from(params.entries())
    .filter(([k]) => k !== 'hash')
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([k, v]) => `${k}=${v}`);
  return entries.join('\n');
}

function verifyInitData(initData: string) {
  const params = new URLSearchParams(initData);
  const hash = params.get('hash');
  if (!hash) throw new Error('Missing hash');

  const dataCheckString = buildDataCheckString(params);
  const secretKey = crypto.createHmac('sha256', 'WebAppData').update(BOT_TOKEN).digest();
  const computed = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex');

  if (computed !== hash) throw new Error('Invalid initData');

  const userJson = params.get('user');
  if (!userJson) throw new Error('Missing user');
  const user = JSON.parse(userJson);

  const authDate = parseInt(params.get('auth_date') || '0', 10) * 1000;
  const ageMs = Date.now() - authDate;
  if (ageMs > 24 * 60 * 60 * 1000) throw new Error('initData expired');

  return { user, authDate } as const;
}

app.post('/auth/telegram', async (req, res) => {
  try {
    const { initData } = req.body;
    if (!initData) return res.status(400).json({ error: 'initData required' });

    // Basic idempotency to smooth bursts
    const idemKey = `idem:auth:${crypto.createHash('sha256').update(initData).digest('hex')}`;
    const set = await redis.set(idemKey, '1', 'NX', 'EX', 15);
    if (!set) return res.status(429).json({ error: 'Try again' });

    const { user } = verifyInitData(initData);

    // Upsert user in DB (pseudo)
    // await db.upsertUser({ telegram_id: user.id, username: user.username, lang: user.language_code })

    const token = jwt.sign(
      { sub: String(user.id), tgid: user.id, scopes: ['app:read', 'app:write'] },
      JWT_SECRET,
      { expiresIn: '10m', issuer: 'miniapp-api' }
    );

    res.json({
      token,
      user: {
        id: user.id,
        username: user.username,
        language_code: user.language_code,
        photo_url: user.photo_url,
      },
      flags: { dailyRewardEnabled: true, abGroup: 'B' },
    });
  } catch (err) {
    // Log with structure; map to 400 for safety
    res.status(400).json({ error: (err as Error).message });
  }
});

// Typical auth middleware for protected routes
function auth(req: any, res: any, next: any) {
  const hdr = req.headers['authorization'] || '';
  const [, token] = hdr.split(' ');
  if (!token) return res.status(401).json({ error: 'Missing token' });
  try {
    req.user = jwt.verify(token, JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

app.get('/inventory', auth, async (req, res) => {
  // Fetch inventory for req.user.sub from Postgres
  res.json({ items: [] });
});

app.listen(3000, () => console.log('API listening on :3000'));

Two cautions we repeat in reviews:

  • Do not use initData as your application session. Verify it once and issue your own token. Re-verify initData when the web app cold-starts, not on every GET /inventory call.
  • Limit JWT lifetime to minutes. Refresh by re-sending initData or via a signed refresh token bound to Telegram id and device metadata.

Picking an auth/session strategy

You have three common patterns for session management. Pick based on your risk tolerance and UX needs.

StrategyHow it worksProsConsWhen to use
Stateless per-requestClient sends initData with every API call; server verifies each timeSimplest backend, no token storeExpensive HMAC checks, larger payloads, more HMAC surfaceVery small apps; prototypes
Short-lived JWT (recommended)Verify initData once per session; issue 5–15 min JWT; re-verify on refreshGood UX, minimal overhead, strong boundary between TG and appNeed key rotation and clock disciplineMost apps; scales well
Session + RefreshJWT + rolling refresh token in DB/RedisFine-grained revocation, device bindingMore state; complexity; invalidation bugsPayments-heavy, higher risk apps

Data modeling and storage

For 90% of Mini Apps, Postgres is the right core with Redis as a hot-path accelerator.

  • Postgres tables: users, sessions (optional), purchases (immutable ledger), inventory, leaderboard_entries, feature_flags, rate_limits (if not in Redis), webhook_events (raw intake + dedup keys).
  • Indexes: (telegram_id) on users, (user_id, created_at desc) on purchases, (score desc) on leaderboards, and (event_id unique) on webhook_events.
  • Redis keys: user:profile:{id} (TTL 5–15 min), idem:{scope}:{hash} (TTL 5–10 min), rl:{user}:{route} (sliding window), flags:{user}.

Avoid over-normalizing early. Keep the purchase ledger append-only; compute balances and entitlements as views or materialized tables refreshed via a worker.

Payments in 2026: Stars and providers

Telegram Stars (introduced on-platform and broadly used by 2025) simplify microtransactions, but you still need solid bookkeeping.

  • Stars: Create a stars invoice through Bot API, store pending purchase row with an invoice_id and idempotency_key. Confirm via webhook and finalize in a transaction: debit Stars, credit inventory, mark completed_at.
  • Provider payments: If using third-party providers, use Telegram’s invoice flow. Track both invoice_payload and provider payment_charge_id. Expect retries and out-of-order updates.
  • Idempotency: On server endpoints that create invoices, accept a client-supplied idempotency_key and enforce with Redis: SET key NX EX 300. On webhook handling, dedup on provider_payment_charge_id or stars_transaction_id.
  • Refunds/chargebacks: Have a reverse ledger entry type with references to the original purchase. Don’t delete; compensate.

A minimal Redis idempotency check:

const ok = await redis.set(`idemp:invoice:${userId}:${idempotencyKey}`, '1', 'NX', 'EX', 300);
if (!ok) return res.status(409).json({ error: 'Duplicate request' });

Real-time and multiplayer

Telegram Mini Apps run in a webview, so WebSockets work, but connections can be killed on app backgrounding. Design for intermittent connectivity.

  • Lightweight realtime: WebSocket or SSE for online status, live auctions, or turn-based moves. Reconnect with exponential backoff.
  • Fast-twitch multiplayer: Host game state servers outside Telegram with authoritative simulation (e.g., Go or Rust process). Treat Telegram as identity and payments layer, not the game loop. UE5/Unity clients can bridge via the webview for menus/economy.
  • Cheating: Never trust the client for authoritative state. Validate moves server-side and sign time-limited action tokens for sensitive flows.

Serverless vs containers vs hybrid

Deployment choice influences latency, cold starts, and ops overhead.

OptionExamplesProsConsSweet spot
Edge/serverlessCloudflare Workers, Vercel EdgeGlobal latency, scale-to-zero, built-in CDNCold starts (improving), limited runtime APIs, long-lived jobs awkwardStatic assets, auth, lightweight APIs
Serverless functionsAWS Lambda, GCP Cloud FunctionsEasy to operate, event-nativeVPC/DB cold starts, concurrency quirksWebhooks, background jobs, medium traffic
Containersk8s, Fly.io, RenderFull control, stable perf, websocketsMore ops, capacity planningHigh RPS APIs, realtime, custom runtimes
Hybrid (recommended)Edge for static/auth, containers for core APIBest of bothComplexity in routing/observability50k–1M MAU Mini Apps

In our experience as builders, the hybrid approach wins: edge for static, token minting, and feature flag prefetch; containers for core API + workers; serverless for bursty webhooks.

Caching, rate limiting, and performance guardrails

  • Cache profile/flags at edge for 60–120 seconds keyed by user + app version. Invalidate on profile changes via cache tags where supported.
  • Apply a per-user token bucket: 60–90 requests/minute, with stricter limits on invoice creation.
  • Batch writes: Queue non-critical writes (analytics events, non-immediate leaderboard updates) to workers.
  • Prepared statements: Use parameterized queries and connection pooling (PgBouncer in transaction mode).
  • Asset budgets: Ship <200 KB initial payload for better open latency in constrained devices.

Security basics you can’t skip

  • Verify initData server-side; never trust a user JSON from the client.
  • Bind critical actions to short-lived server-signed tokens (anti-CSRF in embedded contexts is mostly about not accepting blind requests without a valid app JWT and Telegram user mapping).
  • Audit all role changes and economy operations in an append-only ledger.
  • Secrets rotation: Rotate JWT and Bot tokens quarterly. Keep CA chain issues in mind; plan for cert renewals and outages. We’ve written about edge cases during CA hiccups: Let’s Encrypt stops issuance: potential incident.
  • Error hygiene: Wrap and classify errors to avoid leaking internals to clients. If you like Go, we covered the mindset here: Error handling in Go: stop panicking, start wrapping.

Observability and reliability

  • Logs: JSON with ts, level, user_id, route, latency_ms, invoice_id (if any). Sample at 1:10 for success, 1:1 for 5xx.
  • Traces: Propagate trace IDs through the gateway, API, DB calls, and webhook workers. Visualize top N slow endpoints.
  • Metrics: p50/p95 latency, error rate, DB CPU/IO, queue depth, webhook lag, Telegram API 4xx/5xx.
  • SLOs: 99% of auth responses <200 ms; 99% of invoice creations <500 ms; webhook end-to-end settlement <5 s.
  • Runbooks: Document mitigation for Telegram outages, provider downtime, and CA misconfig.

One dry-wit reminder: if your logs don’t include telegram_user_id, they will include your weekend.

What breaks in production

  • Out-of-order webhooks: Telegram can retry; design idempotent consumers keyed by update_id or business IDs.
  • Double-spend bugs: Missing idempotency on invoice creation or settlement leads to duplicated purchases. Your ledger must make duplicates impossible.
  • Race conditions on inventory: Separate read model from write model; use SELECT … FOR UPDATE or explicit versioning for balance rows.
  • Clock drift: Short-lived JWTs plus skewed server clocks create spurious 401s. Sync with NTP; prefer monotonic timers for rate limits.
  • iOS webview quirks: Backgrounding kills sockets; avoid long single-flight requests during app switches.
  • Expired initData: Telegram-auth auth_date older than 24 hours should be rejected. Offer a quick re-auth path.
  • TLS surprises: Certificates expiring or intermediate chain changes can brick older Android forks. Plan renewals with overlap, and monitor SSL/TLS health.
  • Overzealous caching: Edge caches that ignore Authorization headers leak personalized data. Vary by token/user and set correct cache keys.

Cost and ROI: realistic budgets

You can host a serious Mini App without lighting money on fire. Ballparks we see often (USD/month):

  • Prototype (up to 10k MAU): $150–$600
    • Cloudflare Pro, small Postgres (1–2 vCPU), Redis micro, small container or serverless functions, minimal logging.
  • Growth (50k–200k MAU): $1.5k–$6k
    • Cloudflare Business, Postgres 2–4 vCPU with replica, Redis 1–2 GB, 2–4 app instances, dedicated workers, observability stack.
  • At scale (500k–1M+ MAU): $8k–$30k
    • HA Postgres (8–16 vCPU), Redis cluster, 6–12 app instances across regions, autoscaled workers, beefy logs/traces retention, on-call.

Development time: a lean team can ship a commerce-capable Mini App in 6–10 weeks with this stack, faster if features are narrow and the economy is simple. Expensive mistakes are mostly in payments and state: rebuilding a ledger or re-indexing a 100M-row table after launch is the kind of education nobody wants to pay for twice.

Implementation checklist (condensed)

  • Verify initData and mint 10-min JWT with sub=telegram_id
  • Map Telegram users to users table; index by telegram_id
  • Add Redis-backed idempotency on invoice creation and webhook processing
  • Keep an append-only purchases ledger with reversible entries
  • Edge cache static + profile/flags (60–120s) with correct Vary headers
  • Enforce per-user rate limits and WAF guardrails
  • Add structured logs, traces, alerting on error budgets
  • Run load tests: 2–5x expected RPS, verify DB saturation points

FAQ

How should I verify Telegram Mini App initData?

Compute a data_check_string by sorting all key=value pairs except hash. Derive secret_key = HMAC_SHA256("WebAppData", bot_token), then compute HMAC_SHA256(data_check_string, secret_key) and compare to hash. Also reject if auth_date is older than 24 hours.

Should I re-verify initData on every API call?

No. Verify once when the app loads, then issue a short-lived JWT (5–15 minutes). Re-verify on refresh or when the Mini App cold-starts. This reduces CPU overhead and simplifies auth.

Do I need webhooks if I already poll the Bot API?

Use webhooks. Telegram supports webhooks reliably with retries. Polling wastes cycles and increases latency for payments. Treat webhooks as at-least-once and implement idempotency.

What’s the best database for Mini Apps?

Postgres for core state and economy, Redis for caching/rate limits. Add read replicas if you have heavy read traffic (leaderboards). Avoid exotic databases unless you have a narrow, proven reason.

Can I use serverless for everything?

You can, but long-lived sockets and high-RPS cores often perform better on containers. A hybrid works well: edge/serverless for static, auth, and webhooks; containers for the main API and workers.

How do I prevent duplicate purchases with Stars?

Store a pending purchase row with invoice_id and an idempotency key before creating the invoice. On webhook confirmation, transactionally mark as completed using a unique constraint on the transaction or charge ID.

Key takeaways

  • Verify Telegram initData server-side and issue short-lived JWTs; don’t use initData as your session.
  • Keep a clean boundary: API service, Postgres, Redis, queue/workers, and a dedicated webhook ingestor with idempotency.
  • Treat payments as a ledger with immutable entries and compensations; never delete, only reverse.
  • Prefer a hybrid deployment: edge for static/auth, containers for core API, serverless for bursty webhooks.
  • Invest early in observability and rate limits; they pay for themselves the first time traffic spikes or a provider hiccups.

If you’re building or scaling a Telegram Mini App and want a battle-tested backend without the dead ends, MTBYTE can design and ship it with you. Tell us what you’re building at /contact and we’ll propose a practical plan and stack.

NEXT STEP

Liked the approach?

We apply the same principles to client projects: AI, automation, products that don't die after launch.