Telegram Serverless vous permet d’exécuter le code backend de votre bot et de votre Mini App directement sur l’infrastructure de Telegram — aucun serveur à provisionner, aucun conteneur à maintenir en vie, aucune mise à l’échelle à considérer. Vous écrivez des modules JavaScript simples, les déployez avec une seule commande, et Telegram les exécute dans un sandbox V8 rapide et isolé qui se trouve juste à côté de l’API Bot et d’une base de données intégrée.

Si vous avez déjà connecté un bot à un VPS, une fonction cloud, ou un panneau d’hébergement juste pour répondre à un /start, c’est la partie que vous n’avez plus à faire.

Pourquoi serverless

Un bot Telegram est, dans son essence, un programme qui réagit aux mises à jour. Traditionnellement, vous deviez héberger ce programme quelque part qui est toujours actif, accessible et sécurisé — et ensuite le maintenir ainsi. Telegram Serverless supprime entièrement cette couche :

  • Aucune infrastructure. Il n’y a pas de machine à louer, à patcher ou à surveiller. Votre code s’exécute à la demande et s’adapte automatiquement à votre bot.
  • Identifiants inclus. L’API Bot Telegram, une base de données basée sur SQLite, et les connexions HTTP sortantes sont disponibles pour chaque module dès le départ — rien à installer, aucun identifiant à connecter.
  • Exécution rapide et isolée. Chaque invocation s’exécute dans un isolat V8 léger, à proximité des propres systèmes de Telegram, de sorte que les appels à l’API Bot et à votre base de données sont rapides et fiables.
  • Un véritable flux de travail de développeur. Un projet vit dans un dossier sur votre machine sous contrôle de version. Vous modifiez des fichiers, voyez exactement ce qui a changé, déployez atomiquement, et faites progresser le schéma de votre base de données avec des migrations examinées — la façon dont vous travaillez déjà avec tout le reste.

Le modèle mental : Comment fonctionne le projet

Vous travaillez dans trois endroits, et ils se mappent proprement les uns sur les autres :

Où Ce qui y vit
Votre dossier projet Modules JavaScript — schéma, code partagé, gestionnaires de mise à jour, points d’accès Mini App — et, si vous en avez un, le front-end du Mini App
Le cloud La copie déployée de ces modules, la base de données de votre bot, et les fichiers du Mini App
Le CLI tgcloud Le pont — il vous montre les différences et les synchronise

Vous ne vous connectez jamais en SSH à quoi que ce soit. Vous modifiez les fichiers localement, exécutez npx tgcloud push, et la plateforme s’en charge. Le trafic de votre bot est géré par les modules déployés ; votre base de données persiste entre les invocations.

Un projet n’a que quatre types de code, tous à l’intérieur d’un dossier tgcloud/ :

tgcloud/
├─ endpoints/  # fonctions appelées par votre Mini App
├─ handlers/   # fonctions qui réagissent aux types de mises à jour Telegram
├─ lib/        # code partagé que vous importez de n'importe où
└─ schema.js   # vos tables de base de données

Lorsqu’une mise à jour arrive — un message, une pression sur un bouton, une requête en ligne — Telegram la dirige vers le gestionnaire correspondant (handlers/message.js, handlers/callback_query.js, …) et appelle son export par défaut. Cette fonction communique avec l’API Bot et la base de données via le SDK, puis retourne. C’est toute la boucle. Une mise à jour sans gestionnaire correspondant est simplement ignorée, vous ajoutez donc uniquement les gestionnaires dont vous avez besoin.

Un Mini App est facultatif. Si votre bot en a un, son front-end se trouve dans le même projet, à côté de tgcloud/. Serverless héberge son résultat de build et exécute les endpoints/ qu’il appelle. Voir Front-end Mini App .

Démo rapide

Voici un bot démo complet et fonctionnel. Il répond à chaque message et se souvient combien il en a vu de chaque chat.

tgcloud/schema.js

import { table, integer } from 'sdk/db';

export const counters = table('counters', {
  chatId: integer('chat_id').primaryKey() ,
  seen:   integer('seen').notNull().default(0) ,
});

tgcloud/handlers/message.js

import { api, db } from 'sdk';
import { sql } from 'sdk/db';
import { counters } from '../schema.js';

export default async function (message) {
  const chatId = message.chat.id;

  // Insérer le compteur, ou l'incrémenter si ce chat en a déjà un — et récupérer la
  // ligne résultante dans la même instruction via .returning().
  const [row] = await db.insert(counters)
    .values({ chatId, seen: 1 })
    .onConflictDoUpdate({
      target: counters.chatId,
      set: { seen: sql`${counters.seen} + 1` },
    })
    .returning()
    .run();

  await api.sendMessage({
    chat_id: chatId,
    text: `Bonjour ! Je vous ai vu ${row.seen} message(s).`,
  });
}

Déployez-le :

npx tgcloud push       # téléverser les modules
npx tgcloud migrate    # créer la table `counters`

C’est un bot en direct avec état persistant et sans serveur. Tout ce qu’il contient — api, db, le DSL table() — est décrit dans les sections ci-dessous.

Serverless est un backend général pour les bots Telegram et les Mini Apps, pas un modèle pour un seul type d’application. Il est idéal pour :

  • Bots d’IA conversationnelle qui ont besoin de stocker un état par utilisateur dans une base de données.
  • Backends de Mini App qui stockent des données utilisateur et servent du contenu dynamique.
  • Jeux et Outils — y compris des classements, des quiz et plus encore.
  • Automatisations et Intégrations qui appellent des API HTTP tierces et poussent des résultats dans les chats.

Démarrage

Ce guide vous emmène d’un dossier vide à un bot en direct qui répond aux messages et stocke des données. Il suppose que vous avez Node.js 18 ou plus récent installé et un bot enregistré auprès de @BotFather . À la fin, vous aurez utilisé toutes les commandes dont vous avez besoin au quotidien : push, migrate, run, et status.

Avant toute chose, activez Serverless. Dans @BotFather , ouvrez votre bot → Serverless et activez-le. Cela active la fonctionnalité pour ce bot et déverrouille son jeton d’accès CLI, ses gestionnaires, sa librairie et sa base de données.

1. Créer un projet

La façon la plus rapide de commencer est le créateur de projet, qui initialise un projet et y installe le CLI :

npm create @tgcloud/bot example_bot
cd example_bot

L’argument est le dossier cible : passez . pour initialiser dans le dossier actuel, ou n’importe quel chemin. Cela fonctionne aussi dans un dossier existant et ne surcharge jamais les fichiers que vous avez déjà.

Cela vous donne un projet prêt à être édité :

example_bot/
├─ tgcloud/             # tout ce que la plateforme exécute
│  ├─ handlers/
│  │  └─ message.js     # un gestionnaire de message de démarrage (fait un écho du texte)
│  ├─ lib/              # vos modules partagés vont ici (vide pour commencer)
│  └─ schema.js         # vos tables de base de données
├─ docs/
│  └─ tgcloud-sdk.md    # référence du SDK (pour vous et vos outils d'IA)
├─ AGENTS.md            # orientation pour les assistants de codage IA
├─ package.json
└─ tgcloud.jsonc        # configuration du projet

Les fichiers initialisés sont auto-documentés — chacun contient des exemples commentés de ce que vous pouvez faire ensuite.

Le CLI s’installe dans le projet comme une dépendance de développement locale, donc vous l’exécutez avec npx tgcloud <command> (npx trouve la copie dans le node_modules de votre projet), ou via les raccourcis npm run que le scaffold ajoute à package.json (npm run deploy, npm run status). Par défaut, il n’y a pas de tgcloud global dans votre PATH.

Vous pouvez aussi l’installer globalement — npm install -g @tgcloud/cli — si vous préférez taper un simple tgcloud depuis n’importe où. C’est pratique pour exécuter tgcloud init dans n’importe quel dossier vide, et c’est ce dont la complétion par tabulation a besoin. Quoi qu’il en soit, vous obtenez le même projet.

Construction d’un Mini App ? Commencez plutôt par son front-end. Créez-le avec votre outil habituel, puis exécutez le créateur de projet dans ce dossier. Voir Front-end Mini App .

2. Lier votre bot

Chaque projet est lié à un bot. Connectez-les avec login, qui demande votre jeton d’accès CLI ( @BotFather → votre bot → Serverless → CLI Access → Access token — un jeton distinct de votre jeton d’API de bot ) et le stocke localement :

npx tgcloud login

Le jeton a la forme app<id>:<secret>. Le CLI le garde dans .tgcloud/, qui est ignoré par git, et n’imprime jamais la partie secrète. La connexion est la seule fois où on vous le demande — voir Authentification pour savoir comment les jetons sont résolus en CI.

3. Regarder autour

Deux commandes vous indiquent l’état des choses à tout moment, toutes deux entièrement hors ligne :

npx tgcloud status     # ce qui a changé localement par rapport à la copie déployée
npx tgcloud diff       # les changements ligne par ligne

Juste après init, tout est nouveau et rien n’est encore déployé. status montre les fichiers de démarrage en attente d’être téléversés.

4. Déployer

Envoyez vos modules dans le cloud :

npx tgcloud push

push téléverse chaque module modifié dans un lot atomique et met à jour votre enregistrement local de ce que le cloud détient maintenant. Votre bot est en ligne : ouvrez-le dans Telegram et envoyez-lui un message — le gestionnaire de démarrage fait un écho de celui-ci.

Le déploiement ne touche jamais votre base de données. La poussée de code et le changement de schéma de votre base de données sont des étapes délibérément séparées, donc un déploiement de code ne peut jamais vous surprendre avec une migration de données. C’est ce à quoi sert la prochaine étape.

5. Ajouter une table de base de données

Faisons en sorte que le bot se souvienne de quelque chose. Ouvrez tgcloud/schema.js et déclarez une table :

import { table, integer, text, sql } from 'sdk/db';

export const messages = table('messages', {
  id:      integer('id').primaryKey({ autoIncrement: true }) ,
  chatId:  integer('chat_id').notNull() ,
  text:    text('text') ,
  created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
});

Déployez le schéma, puis appliquez-le à la base de données :

npx tgcloud push       # téléverse le nouveau schéma
npx tgcloud migrate    # crée la table `messages`

push signale que le schéma est désynchronisé et vous montre le changement en attente, mais n’applique rien. migrate vous guide à travers le changement et, après votre confirmation, crée la table. Ce modèle en deux étapes — et ce qui se passe avec les changements plus risqués comme les suppressions — est couvert dans Migrations .

6. Stocker et lire des données

Utilisez maintenant la table depuis votre gestionnaire. Modifiez tgcloud/handlers/message.js :

import { api, db } from 'sdk';
import { eq } from 'sdk/db';
import { messages } from '../schema.js';

export default async function (message) {
  // Sauvegarder ce message.
  await db.insert(messages)
    .values({ chatId: message.chat.id, text: message.text })
    .run();

  // Compter combien nous en avons stocké pour ce chat.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Sauvegardé. C'est ${count} message(s) de ce chat jusqu'à présent.`,
  });
}

Déployez le gestionnaire mis à jour avec npx tgcloud push, puis envoyez quelques messages à votre bot et regardez le compte augmenter. La base de données persiste entre les invocations — c’est la mémoire de votre bot.

7. Tester sans déployer

Vous n’avez pas besoin de déployer pour essayer un changement. npx tgcloud run exécute un gestionnaire sur la plateforme en utilisant vos fichiers locaux, sans les publier :

npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "bonjour" }'

L’argument est la charge utile que votre gestionnaire reçoit — pour handlers/message, un Message — écrit en JSON5 (vous pouvez donc omettre les guillemets pour les clés). La commande imprime tout ce que le gestionnaire a enregistré avec console.*, la valeur de retour, et combien de temps cela a pris. C’est la boucle la plus serrée pour itérer sur la logique — aucun déploiement, aucune attente d’un vrai message.

8. Rester synchronisé

Au fur et à mesure que vous travaillez, une poignée de commandes maintiennent votre projet local et le cloud alignés : npx tgcloud status montre ce qui a changé, npx tgcloud push déploie, npx tgcloud pull ramène votre projet local dans la ligne du cloud, npx tgcloud fetch vérifie ce qui est déployé sans toucher vos fichiers, et npx tgcloud reset abandonne les changements locaux.

Si deux personnes (ou deux machines) déploient sur le même bot, la plateforme détecte le conflit et push s’arrête pour vous permettre de pull d’abord — vous ne pouvez jamais écraser silencieusement le travail de quelqu’un d’autre. Voir Rester synchronisé .

Construire avec l’IA

Vous préférez construire avec un assistant IA — ou si le seul codeur de votre équipe est une IA ? Vous pouvez toujours expédier un bot. Nous avons fait un premier pas pour qu’un agent IA se sente chez lui dans un projet : chacun de nouveaux est initialisé avec un AGENTS.md et une référence docs/tgcloud-sdk.md que les outils de codage agentiques lisent automatiquement.

Associé à un runtime petit et autonome — un seul SDK, pas de paquets npm à gérer — cela donne à l’assistant un bon départ sur les conventions que le codage générique manque souvent ici : imports relatifs avec .js, pas de clés étrangères, chaque appel db est asynchrone, un gestionnaire par type de mise à jour, et le flux en deux étapes push/migrate.

Essayez-le :

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # ou Claude Code, Cursor, … — n'importe quel agent qui lit AGENTS.md

Ensuite, demandez simplement, en langage clair :

Écris un bot qui se souvient de la liste de tâches de chaque personne — ajoute un élément quand ils envoient du texte, et affiche la liste entière quand ils envoient /list.

L’assistant modifie schema.js et vos gestionnaires pour vous ; vous examinez, testez un changement instantanément avec npx tgcloud run, puis mettez en ligne avec npx tgcloud push et npx tgcloud migrate. AGENTS.md fait partie de votre projet — modifiez-le à mesure que le bot grandit afin que les conseils restent précis.

En déplacement avec BotFather

Réduit à votre téléphone ? L’ensemble du projet vit aussi dans @BotFather — ouvrez votre bot → Serverless et vous obtenez tout ce que le CLI gère, sur un écran tactile :

  • Gestionnaires — créez, modifiez et testez des gestionnaires de mise à jour ; BotFather maintient le webhook synchronisé avec les gestionnaires que vous avez (le même statut Synchronisé / Désynchronisé que le CLI signale).
  • Points d’accès — les fonctions que votre Mini App appelle.
  • Librairie — vos modules lib/ partagés.
  • Base de données — modifiez schema.js dans la même syntaxe de type Drizzle, examinez les changements en attente et appliquez-les ; Enregistrer déploie.
  • Fichiers Mini App — le front-end que vos utilisateurs ouvrent dans Telegram.
  • Accès CLI — récupérez le jeton d’accès CLI ici lorsque vous êtes de retour à un clavier.

C’est le même projet cloud, donc vous pouvez démarrer un gestionnaire sur votre téléphone et le npx tgcloud pull sur votre ordinateur portable plus tard — rien n’est lié à un seul client. L’exécution d’un gestionnaire affiche même sa sortie Console directement dans le chat, tout comme npx tgcloud run .

Projets et modules

Un projet serverless est un dossier ordinaire sous contrôle de version. Tout ce que la plateforme exécute se trouve dans un seul dossier, tgcloud/; le reste du projet vous appartient — un front-end de mini-app, la configuration, la documentation. Il n’y a pas d’étape de build pour le bot, pas de node_modules à l’exécution, et pas de point d’entrée serveur.

Anatomie d’un projet

example_bot/
├─ tgcloud/             # la partie déployée — rien d'autre ne l'est
│  ├─ handlers/         # gestionnaires de mise à jour — plat, un seul niveau
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # fonctions appelées par votre Mini App — plat, un seul niveau
│  │  └─ getProfile.js
│  ├─ lib/              # modules partagés ; sous-dossiers autorisés
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # schéma de base de données — un fichier
└─ .tgcloud/            # état du CLI, ignoré par git

Seuls les fichiers .js dans tgcloud/ — schema.js, lib/, handlers/, endpoints/ — sont déployés. Tout le reste — Markdown, fichiers de configuration, le dossier .tgcloud/, tout ce qui est en dehors de tgcloud/ — reste sur votre machine. Les noms de modules sont relatifs à tgcloud/. Le fichier tgcloud/handlers/message.js est le module handlers/message, et c’est le nom que npx tgcloud run prend.

Mise à niveau à partir d’un ancien projet ? Les projets créés avant le CLI 0.2.0 gardaient schema.js, lib/ et handlers/ à la racine du projet. Exécutez npx tgcloud upgrade une fois pour les déplacer dans tgcloud/.

schema.js — votre base de données. Il déclare les tables comme exportations nommées utilisant le DSL de schéma et vit dans tgcloud/ comme un fichier unique. Il est déployé comme tout autre module, mais le déployer ne change jamais la base de données — les changements de schéma sont appliqués séparément avec npx tgcloud migrate. Voir La base de données .

lib/ — code partagé, tout ce que vous voulez réutiliser entre les gestionnaires : assistants purs, couches d’accès à la base de données, formatage, intégrations avec des services extérieurs. lib/ est le seul répertoire qui peut contenir des sous-dossiers (lib/internal/util.js, lib/payments/stripe.js), vous pouvez donc organiser une base de code plus importante comme vous le souhaitez. Les modules dans lib/ ne sont jamais invoqués directement par la plateforme ; ils existent pour être importés par les gestionnaires et par eux-mêmes.

handlers/ — les points d’entrée de votre bot. Chaque fichier correspond à un type de mise à jour Telegram, et la plateforme dirige chaque mise à jour entrante vers le gestionnaire correspondant :

Fichier Gère
handlers/message.js Nouveaux messages entrants
handlers/inline_query.js Nouvelles requêtes en ligne entrantes
handlers/callback_query.js Nouvelles requêtes de rappel entrantes
… tout autre type de mise à jour de l’API Bot

handlers/ est plat — pas de sous-dossiers. Le export default d’un gestionnaire est la fonction que la plateforme appelle (voir Gestionnaires ).

Un type de mise à jour est géré uniquement si son fichier de gestionnaire existe et n’est pas vide. S’il n’y a pas de handlers/<type>.js — ou si le fichier est vide — les mises à jour de ce type sont ignorées, et la plateforme ne lance rien pour elles. Gardez donc uniquement les gestionnaires dont vous avez réellement besoin : chacun que vous ajoutez est un autre type de mise à jour auquel votre bot se réveille pour traiter, et omettre le reste signifie que Telegram ne lance pas votre code pour des mises à jour que vous jetteriez de toute façon.

Pour initialiser un nouveau gestionnaire, exécutez npx tgcloud add handlers/<type> .

endpoints/ — le côté serveur de votre Mini App. Chaque fichier est une fonction que le Mini App appelle par nom, donc endpoints/getProfile.js répond à POST /api/getProfile. La plateforme vérifie que l’appel provient d’un utilisateur Telegram à l’intérieur de votre Mini App avant que votre code ne s’exécute, et vous transmet cet utilisateur. endpoints/ est plat, comme handlers/. Voir Points d’accès .

.tgcloud/ — état local à la machine géré entièrement par le CLI, y compris vos identifiants sauvegardés. Il est ignoré par git, et vous ne devriez jamais lire ou écrire dedans à la main — utilisez plutôt les commandes du CLI.

Le système de modules

À l’exécution, un module peut voir exactement deux choses : le SDK de la plateforme et les autres modules de votre projet. Il n’y a pas de paquets npm, pas de système de fichiers, et pas de réseau sauf à travers le fetch du SDK.

Importez vos propres modules comme vous le feriez dans n’importe quel projet JavaScript — par chemin relatif, avec l’extension .js — et le SDK par son nom :

// tgcloud/handlers/message.js
import { users } from '../schema.js';              //  le module de schéma
import { addItem } from '../lib/cart.js';          //  un module lib
import { format } from '../lib/internal/fmt.js';  //  module lib imbriqué
import { db, api, fetch } from 'sdk';              //  le SDK de la plateforme
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        //  un voisin
import { users } from '../schema.js';

Un import relatif doit se terminer par .js et ne peut pas sortir de tgcloud/ :

import { users } from '../schema';       //  ajouter le .js
import config from '../../config.js';    //  en dehors de tgcloud/
import { z } from 'zod';                 //  pas de paquets npm

Chaque module a également un nom — son chemin à l’intérieur de tgcloud/ sans l’extension, tel que schema, lib/cart ou handlers/message. Ce nom est ce que npx tgcloud run et la sortie du CLI utilisent, et un module peut également être importé par celui-ci (from 'lib/cart', sans extension). Préférez les imports relatifs dans votre code. Ce sont ceux que votre éditeur comprend — saut vers la définition, auto-complétion, renommage.

Ce qui est disponible à l’exécution est exactement deux choses : sdk et ses sous-modules (sdk/db, sdk/api, sdk/fetch) — toute la surface de la plateforme, voir Le SDK — et vos propres modules dans tgcloud/. C’est la liste complète. Si votre code import autre chose, cela ne se résoudra pas. Cette contrainte est ce qui rend les modules rapides à charger et sûrs à exécuter.

Gestionnaires

Un gestionnaire est un module dans handlers/ dont l’export par défaut est invoqué par la plateforme lorsqu’une mise à jour correspondante arrive.

// handlers/message.js
import { api } from 'sdk';

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Vous avez dit : ${message.text ?? '(pas de texte)'}`,
  });
}

Un gestionnaire reçoit la charge utile de la mise à jour comme argument — la plateforme vous décompresse la mise à jour Telegram . handlers/message.js reçoit le Message (c’est-à-dire update.message) ; handlers/callback_query.js reçoit la CallbackQuery ; et ainsi de suite. Le deuxième argument du gestionnaire est un objet de contexte par invocation, ctx. Il transporte la Update brute comme ctx.update — utilisez-le lorsque vous avez besoin de quelque chose en dehors de la charge utile, comme update_id.

Un gestionnaire peut être async (il l’est généralement) et peut retourner une valeur. Il accède à l’API Bot, à la base de données et aux requêtes HTTP sortantes via le SDK.

Vous n’avez pas besoin d’une vraie mise à jour pour tester un gestionnaire. npx tgcloud run l’exécute sur la plateforme avec la charge utile que vous fournissez et votre code local actuel :

npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "salut" }'

L’argument est la charge utile — le même objet que votre gestionnaire reçoit — en JSON5. Pour fournir le ctx du gestionnaire (son deuxième argument), ajoutez --ctx, par exemple --ctx '{ update: { update_id: 1 } }'. Cela s’exécute contre vos fichiers locaux, donc vous pouvez essayer les changements avant de les déployer. Voir run .

Points d’accès

Un point d’accès est un module dans endpoints/ que votre Mini App appelle — l’endroit pour tout ce que le front-end ne peut pas faire par lui-même, comme lire et écrire dans la base de données, appeler l’API Bot, ou parler à des services externes avec vos secrets.

// endpoints/getProfile.js
import { db, EndpointError } from 'sdk';
import { eq } from 'sdk/db';
import { profiles } from '../schema.js';

export default async function (input, ctx) {
  const user = ctx.initData.user;
  const profile = await db.select().from(profiles).where(eq(profiles.userId, user.id)).get();
  if (!profile) throw new EndpointError('Profile not found', { userId: user.id });
  return profile;
}

Le premier argument, input, est l’objet JSON envoyé par le Mini App. Le second, ctx, contient ctx.initData — les données d’initialisation du Mini App (user, chat, start_param, auth_date, …), déjà vérifiées par la plateforme. Il a la même structure que Telegram.WebApp.initDataUnsafe côté client, mais ici vous pouvez lui faire confiance. Un appel sans données d’initialisation valides est rejeté avec 401 avant que votre code ne s’exécute, donc ctx.initData.user est vraiment la personne qui a ouvert votre Mini App. Les points d’accès ne peuvent être appelés que de cette manière — depuis le Mini App de votre bot, par un utilisateur Telegram — il n’y a donc pas d’API publique à protéger et pas de connexion à construire.

Ce que le point d’accès retourne est renvoyé sous forme de JSON. Pour refuser un appel, throw new EndpointError(description, parameters?). Le Mini App reçoit la description et l’objet parameters tels quels. Si vous avez besoin de ramifier en fonction du type d’erreur dans le front-end, mettez un code dans parameters — { code: 'OUT_OF_STOCK' }. Toute autre exception est signalée comme une erreur interne générique, et le vrai message ne va que dans vos journaux, donc les détails internes ne fuient pas vers le client.

Appel d’un point d’accès depuis le Mini App

Le Mini App appelle un point d’accès par son nom avec Telegram.WebApp.Serverless.call du SDK officiel des Mini Apps , telegram-web-app.js — le script que votre Mini App inclut déjà. Il envoie les données d’initialisation pour vous et rappelle avec une erreur ou le résultat du point d’accès :

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — la description ; err.parameters — ce que votre EndpointError a transporté
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

L’entrée est facultative et doit être un objet simple. Sans elle, l’appel est Serverless.call('getProfile', callback).

err.type est 'ENDPOINT_ERROR' lorsque votre point d’accès a lancé une EndpointError. Toute autre valeur signifie que la plateforme a refusé l’appel ou que le point d’accès a échoué de manière inattendue, par exemple 'UNAUTHORIZED' lorsque les données d’initialisation sont manquantes ou invalides.

Les noms des points d’accès sont comme des identifiants JavaScript — lettres, chiffres et _, ne commençant pas par un chiffre (getProfile, place_order). Initialisez-en un avec npx tgcloud add endpoints/<name> , et essayez-le sans le Mini App avec run , en vous fournissant vous-même les données d’initialisation :

npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1, first_name: "Ann" } } }'

Ce qui est déployé

Lorsque vous exécutez npx tgcloud push, le CLI rassemble tous les fichiers .js sous schema.js, lib/, handlers/, et endpoints/, et envoie cet ensemble exact comme espace de modules de votre projet — ainsi que le dossier de build du Mini App, si tgcloud.jsonc en nomme un (voir ci-dessous). Tout ce qui est présent dans le cloud mais absent de votre projet est supprimé, de sorte que l’état déployé reflète toujours votre dossier — les suppressions incluses. Rien en dehors de tgcloud/ n’est déployé en tant que code — le reste de la racine du projet vous appartient. À l’intérieur de tgcloud/, un fichier .js qui n’est pas dans l’un de ces emplacements (tgcloud/util.js, tgcloud/helpers/x.js) est signalé, pour qu’il ne soit pas déployé silencieusement. Le Markdown, les fichiers cachés et .tgcloud/ ne sont jamais déployés.

Front-end Mini App

La racine du projet vous appartient, et c’est l’endroit naturel pour le front-end de votre Mini App — construit avec React, Vue, Svelte ou du HTML simple, par Vite ou tout autre bundler. Serverless héberge son résultat de build à côté de votre bot. Un seul push déploie le code et les fichiers statiques ensemble, et l’application est servie à https://app<app_id>.tgcloud.ai/ (push imprime l’adresse exacte — utilisez-la comme URL de votre Mini App dans @BotFather ).

Signalement d’abus. Les Mini Apps sur tgcloud.ai sont publiés par des développeurs indépendants. Pour signaler un contenu illégal ou un abus sur un sous-domaine app<app_id>.tgcloud.ai, envoyez un e-mail à [email protected] .

Pour commencer, créez d’abord le front-end, avec l’outil de votre choix, puis ajoutez Serverless par-dessus :

npm create vite@latest example_app   # ou create-vue, …
cd example_app
npm create @tgcloud/bot .

Le créateur de projet ne surcharge jamais vos fichiers. Il ajoute le dossier tgcloud/ à côté du front-end, reconnaît le dossier de build, et ajoute un script npm run deploy qui construit l’application et la déploie avec le bot.

Ce qui est hébergé est défini dans tgcloud.jsonc, le fichier de configuration du projet à la racine du projet :

{
  "$schema": "./node_modules/@tgcloud/cli/schema/tgcloud.json",
  "static": {
    "source": "dist",
    "spa": true,
    "immutable": ["/assets/*"],
    "headers":   [{ "path": "/*", "set": { "Referrer-Policy": "no-referrer" } }],
    "redirects": [{ "from": "/old", "to": "/new", "status": 301 }]
  }
}

Seul "source" est obligatoire. C’est le dossier de build, relatif à la racine du projet. Les autres clés de "static" configurent la manière dont il est servi (ci-dessous). init crée tgcloud.jsonc pour vous. Lorsqu’il reconnaît le front-end, il configure également "source" :

Front-end init définit "source" à
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular rien — configurez d’abord une exportation statique, puis définissez "source" à son dossier vous-même

Avec toute autre configuration, définissez "source" à votre dossier de build vous-même.

La ligne $schema donne à votre éditeur la complétion et la validation pour chaque clé. Le fichier est en JSON avec des commentaires et des virgules finales autorisées. Si un autre de vos outils doit le lire comme du JSON brut, nommez-le tgcloud.json à la place — il doit alors être du JSON strict.

Construisez avant de pousser. push téléverse le dossier exactement tel qu’il est sur le disque — sauf les fichiers et dossiers dont le nom commence par un point, qui sont ignorés — et n’exécute jamais votre build, donc un simple npx tgcloud push après avoir modifié le front-end déploierait le build précédent. Utilisez npm run deploy (il exécute votre script build, puis tgcloud push), ou construisez vous-même d’abord.

Les options de service :

  • spa — sert index.html pour les chemins qui n’existent pas et n’ont pas d’extension de fichier, donc un routeur côté client fonctionne au rechargement. Les chemins avec une extension obtiennent toujours un 404.
  • immutable — modèles pour les fichiers avec hash de contenu (les /assets/* de Vite) que les navigateurs peuvent mettre en cache pendant un an. Utilisez-le uniquement pour les fichiers avec hash de contenu. Tout autre fichier correspondant ici resterait obsolète dans les navigateurs pendant un an, et il n’y a aucun moyen de le rappeler.
  • headers — en-têtes de réponse pour les chemins auxquels une règle correspond (un chemin exact, ou un préfixe se terminant par /*). Une valeur null supprime un en-tête.
  • redirects — règles de redirection, où la première correspondance l’emporte. Un fichier existant l’emporte toujours sur une règle. status est 301 sauf si vous le définissez.
  • "static": "dist" — un raccourci pour { "source": "dist" } lorsque vous n’avez besoin de rien d’autre.
  • "static": false — supprime le site déployé lors du prochain push. Supprimer la clé à la place arrête simplement ce projet de gérer les fichiers statiques, et ce qui est déployé reste.

Le site est servi à partir de la racine de son propre hôte, donc la base par défaut de votre bundler (/) est correcte. Telegram Web ouvre les Mini Apps dans un iframe, n’ajoutez donc pas de politique X-Frame-Options ou frame-ancestors restrictive.

Le même hôte répond à vos points d’accès sous /api/ — c’est là que Telegram.WebApp.Serverless.call('getProfile', …) va. Ne mettez pas de vos propres fichiers sous /api/ dans le dossier de build.

La base de données

Chaque bot obtient sa propre base de données — un stockage basé sur SQLite qui persiste entre les invocations et est disponible pour chaque module via db. Vous décrivez vos tables dans schema.js avec un petit DSL typé ; vous les lisez et les écrivez avec un constructeur de requêtes fluide ; et vous les faites évoluer avec des migrations examinées.

Connaissez Drizzle ORM ? Alors vous savez déjà comment parler à la base de données ici. Le DSL de schéma et le constructeur de requêtes suivent Drizzle — les constructeurs de colonnes, select().from().where(), les opérateurs, onConflictDoUpdate, .returning(), et le tag sql se comportent tous comme vous vous y attendriez, donc lire et écrire des données est l’API familière que vous utilisez déjà. Vous l’importez simplement depuis sdk/db, et quelques spécificités de la plateforme (notamment pas de clés étrangères ) sont soulignées là où elles se présentent.

Déclaration des tables

Les tables sont des exportations nommées dans schema.js. Appeler table() construit une description au moment du chargement (cela ne touche pas la base de données) ; la plateforme découvre les tables exportées lorsque vous déployez schema.js et migre la base de données pour qu’elle corresponde.

import { table, integer, text, boolean, json, index, sql } from 'sdk/db';

export const users = table('users', {
  id:      integer('id').primaryKey({ autoIncrement: true }) ,
  tgId:    integer('tg_id').unique() ,
  name:    text('name').notNull() ,
  lang:    text('lang').default('en') ,
  isAdmin: boolean('is_admin').default(false) ,
  prefs:   json('prefs') ,
  created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
}, (t) => ({
  createdIdx: index('idx_users_created').on(t.created) ,
}));

table(nom, colonnes, extras?) :

  • nom est le nom SQL de la table ;
  • colonnes est une carte de propriété JS → définition de colonne ;
  • extras est une fonction de rappel facultative (t) => ({ … }) où t expose les colonnes (t.created est une référence à cette colonne) — déclarez des index et des contraintes au niveau de la table ici.

Types de colonnes

Factory Type SQLite Notes
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB lit/écrit Uint8Array
boolean() INTEGER stocké comme 0/1, lu comme true/false
json() TEXT JSON.stringify / JSON.parse auto

Le nom de la colonne est facultatif — omettez-le et la clé JS est utilisée. L’option mode contrôle comment les valeurs sont converties entre SQLite et JavaScript.

mode Stocké comme Valeur JS
boolean INTEGER 0/1 boolean
json TEXT (JSON) objet/tableau quelconque
timestamp INTEGER (secondes unix) Date
timestamp_ms INTEGER (ms unix) Date
bytes BLOB Uint8Array

boolean() et json() sont des raccourcis pour integer(nom, { mode: 'boolean' }) et text(nom, { mode: 'json' }).

Un blob() lit et écrit un Uint8Array — le runtime n’a pas de Buffer Node (et un Buffer est une sous-classe de Uint8Array, donc c’est le type de base portable). Le mode ci-dessus ne régit que la façon dont une valeur est encodée, indépendamment du type de stockage de la colonne : ainsi blob('col', { mode: 'json' }) est l’équivalent BLOB de json() — le même encodage JSON, stocké dans une colonne BLOB plutôt que TEXT.

TL;DR : blob() utilise Uint8Array. mode contrôle l’encodage, pas le stockage, donc blob(..., { mode: 'json' }) stocke du JSON sous forme de BLOB, tandis que json() le stocke sous forme de TEXTE.

Modificateurs de colonne

Les modificateurs de colonne se chaînent à une colonne.

integer('id').primaryKey({ autoIncrement: true })
text('name').notNull()
text('tg').unique()
text('lang').default('en')
integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`)
text('slug').generatedAlwaysAs(sql`lower(name)`, { mode: 'stored' })  // ou 'virtual'
text('email').deprecated('remplacé par login')   // marque la colonne pour suppression

.default() sur une colonne json() encode la valeur pour vous ; un sql…``default est passé tel quel..deprecated()` est terminal — voir Migrations .

Index et contraintes

Les index et les contraintes sont déclarés dans la fonction extras, où les colonnes sont dans la portée.

table('t', { /* … */ }, (t) => ({
  uq:     unique('uq_email').on(t.email) ,
  chk:    check('chk_done', sql`${t.done} in (0, 1)`) ,
  idx:    index('idx_name').on(t.col) ,
  uidx:   uniqueIndex('uidx_email').on(t.email) ,
  lower:  index('idx_lower').on(sql`lower(${t.email})`) ,          // index d'expression
  active: index('idx_active').on(t.userId).where(sql`done = 0`) ,  // index partiel
}));

Les modificateurs au niveau de la table se chaînent après table(...) : .strict(), .withoutRowid(), .deprecated('raison').

Pas de clés étrangères

Le runtime s’exécute avec PRAGMA foreign_keys désactivé. Une clé étrangère déclarée serait silencieusement inerte — pas de cascades, pas de protection contre les orphelins — ce qui est pire que de n’en avoir aucune du tout, donc le DSL la rend impossible. Notamment, .references() et foreignKey() au niveau de la table lancent une erreur lors de la déclaration, et un schéma qui les utilise ne sera pas déployé.

Vous devriez modéliser les relations avec des colonnes simples (userId: integer('user_id')) et faire respecter l’intégrité dans le code de votre application : insérer les parents avant les enfants, supprimer les enfants avant les parents, gérer correctement les erreurs et nettoyer les orphelins avec une LEFT JOIN … WHERE parent.id IS NULL lorsque vous en avez besoin.

L’absence de clés étrangères est une contrainte délibérée, pas un oubli. Tenez-en compte dès le début lors de la planification de votre bot.

Requêtes

db est un constructeur de requêtes fluide. Chaque requête est asynchrone — les méthodes terminales (.all(), .get(), .values(), .run()) retournent des Promesses, donc toujours await.

import { db } from 'sdk';
import { eq, and, desc, asc, count, sql } from 'sdk/db';
import { users, todos } from '../schema.js';

await db.select().from(todos).all();                          // toutes les lignes
await db.select().from(todos).where(eq(todos.id, 1)).get();   // première ligne ou null
await db.select().from(todos).values();                       // lignes comme tableaux de valeurs

await db.select().from(todos)
  .where(and(eq(todos.userId, uid) , eq(todos.done, false)))
  .orderBy(desc(todos.priority) , asc(todos.id))
  .limit(10).offset(20)
  .all();

// projection personnalisée : { alias: columnRef | sqlExpr | aggregate }
await db.select({ id: todos.id, title: todos.text, n: count() })
  .from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();

// compte de lignes — une aide, pas une terminal de constructeur :
await db.$count(todos);                        // toutes les lignes
await db.$count(todos, eq(todos.done, false)); // avec un filtre
  • Chaînable : .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Terminaux : .all(), .get(), .values().
  • Comptez les lignes avec db.$count(table, where?) (ou count() dans une projection).

Insérer, mettre à jour, supprimer :

await db.insert(todos).values({ userId: 1, text: 'Acheter du lait' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // lot
await db.insert(todos).values({ text: 'X' }).returning().run();        // RETURNING *

await db.insert(users).values({ tgId: 42, name: 'Ann' })
  .onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run();

await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();

Notez qu’une insertion par lots est une seule instruction, elle est donc limitée par la limite de variables de SQLite (lignes × colonnes) ; si vous la dépassez, l’insertion échoue — divisez-la vous-même.

Les opérateurs sont importés de sdk/db :

import {
  eq, ne, gt, gte, lt, lte,
  like, notLike,
  isNull, isNotNull, and, or, not,
  between, notBetween, inArray, notInArray,
  count, sum, avg, min, max,
  asc, desc,
} from 'sdk/db';

.where(a, b) avec plusieurs arguments équivaut à and(a, b). Le deuxième argument d’une comparaison est une valeur par défaut, mais peut être une autre colonne ou sql`…` — par exemple eq(a.x, b.y). Les agrégats (count/sum/avg/min/max) sont des fragments sql pour une projection .select({ … }).

SQL brut — lorsque le constructeur n’est pas suffisant, passez au SQL brut. Le mode est choisi par la méthode : db.run pour les écritures, db.all pour de nombreuses lignes, db.get pour une ligne, db.values pour des lignes comme tableaux positionnels.

await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 });
// → { rowsAffected: 3, lastInsertRowid: 0, rows: [] }

await db.all(sql`SELECT * FROM todos WHERE done = ${false}`);
await db.get(sql`SELECT count(*) AS c FROM todos`);
await db.values(sql`SELECT id, text FROM todos`);   // → [[1, 'acheter du lait']]

Une écriture se résout en un résultat d’exécution — rowsAffected, lastInsertRowid, et rows, qui contiennent une clause RETURNING si le SQL en avait une. Utilisez db.values lorsqu’une requête projette deux colonnes du même nom (une jointure comme SELECT u.id, o.id) : db.all indexe les lignes par nom de colonne, donc le deuxième id écraserait le premier.

Le tag sql`…` transforme ${valeur} en paramètre lié et ${table.colonne} en identifiant, et fusionne des fragments sql imbriqués. Utilisez sql.raw('…') pour une chaîne littérale sans paramètres.

Les requêtes brutes ne sont pas liées à une table, donc leurs lignes reviennent sans conversion de mode — un booléen est 0/1, une colonne JSON est une chaîne, un horodatage est un nombre. Seul le constructeur lié à la table convertit les valeurs.

Migrations

Votre base de données évolue au fur et à mesure que votre bot grandit. La plateforme maintient cela en sécurité en séparant le déploiement du code du changement des données, et en classifiant chaque changement de schéma par son niveau de risque.

Le déploiement ne touche jamais la base de données. Lorsque vous npx tgcloud push un schema.js modifié, la plateforme enregistre le nouveau schéma et vous indique ce que la base de données devrait changer — mais n’applique rien :

npx tgcloud push       # déployer schema.js ; signale les changements DB en attente
npx tgcloud migrate    # examiner et appliquer

npx tgcloud migrate calcule la différence entre votre schéma et la base de données en direct et vous guide étape par étape avec un compteur [N/M] en cours :

  • Un résumé bref de tous les changements en attente.
  • Changements sûrs, appliqués ensemble en une seule étape, après confirmation.
  • Avertissements (suppressions, opérations lentes), un par un, chacun confirmé séparément.
  • Changements manuels, présentés avec une raison et une action suggérée, non appliqués automatiquement.
  • Objets non documentés (dans la base de données mais pas dans votre schéma), présentés pour information, non appliqués.

Cela se termine par un résumé : appliqués, ignorés, en attente de correction manuelle, non dans le schéma.

Voir migrate pour les drapeaux (--dry-run, --safe, --yes, --local).

Suppression d’éléments

Supprimer une table ou une colonne de schema.js ne la supprime pas — cela rendrait une suppression accidentelle catastrophique. Pour supprimer quelque chose, marquez-la comme obsolète :

// supprimer une colonne
text('email').deprecated('remplacé par login')

// supprimer une table entière
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('inutilisé');

Lors du prochain migrate, l’objet obsolète apparaît comme une suppression de statut d’avertissement que vous confirmez individuellement. Une fois qu’il a disparu, supprimez la déclaration.

Changement du type d’une colonne

Les changements de type sont manuels — SQLite ne peut pas toujours les effectuer sur place, et la coercition des valeurs existantes est un choix. migrate montrera le changement et sa justification ; effectuez-le vous-même avec SQL brut (db.run(...)), typiquement en créant une nouvelle colonne ou table, en copiant les données, et en échangeant.

Le SDK

À l’exécution, un module a une seule librairie : sdk. Elle regroupe les trois choses dont un backend de bot a besoin — une base de données, l’API Bot Telegram, et les requêtes HTTP sortantes — sans rien à installer et sans identifiants à configurer. La base de données (db) est couverte dans La base de données ; cette section couvre api, fetch, et la globale console.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // toute la surface
// ou à partir de sous-modules :
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Import Ce que c’est
db Base de données — constructeur de requêtes et DSL de schéma → La base de données
api API Bot Telegram — api.sendMessage(...) → ci-dessous
fetch HTTP sortant → ci-dessous
InputFile Un fichier (octets + nom) à télécharger — fonctionne avec api et fetch → Fichiers
EndpointError Refuse l’appel d’un Mini App avec un message qu’il peut afficher → Points d’accès

Importez vos propres modules de projet par chemin relatif avec l’extension .js (from '../schema.js', from '../lib/cart.js'). Voir Le système de modules .

L’API Bot

api vous donne toute l’ API Bot Telegram . Appelez n’importe quelle méthode comme api.<méthode>(params). Chaque méthode actuelle — et future — de l’API Bot fonctionne sans mise à jour du SDK.

import { api } from 'sdk';

const me = await api.getMe();                            // → le résultat déballé
await api.sendMessage({ chat_id: id, text: 'Bonjour !' });
await api.editMessageText({ chat_id, message_id, text: 'Mis à jour' });
await api.answerCallbackQuery({ callback_query_id, text: 'Terminé' });

L’enveloppe de réponse est déballée. L’API Bot enveloppe normalement les résultats dans { ok: true, result: … }. api retourne directement le result — getMe() se résout à l’objet utilisateur, pas à un wrapper. Les paramètres utilisent les noms snake_case de l’API Bot (chat_id, message_id, reply_markup, …).

Les échecs lancent BotApiError. Lorsque l’API Bot retourne { ok: false }, l’appel lance une BotApiError au lieu de retourner une valeur fausse, vous ne pouvez donc pas l’ignorer par accident. L’erreur porte .code (le error_code de l’API Bot), .description (le message lisible par l’homme), .method (quelle méthode a échoué), et .parameters (données supplémentaires telles que retry_after sur un 429, ou migrate_to_chat_id). Capturez-la pour gérer un échec attendu et relancez le reste :

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = le message est déjà parti ; ce n'est pas grave ici.
  } else {
    throw e;
  }
}

Fichiers

Travaillez avec des octets de fichier, pas seulement des file_id — téléversez de nouveaux fichiers et téléchargez ceux existants.

Téléverser — InputFile. Encapsulez des octets bruts dans un InputFile (un simple conteneur : octets, un nom de fichier et un type MIME facultatif) et passez-le partout où une méthode de l’API Bot attend un fichier :

import { api, InputFile } from 'sdk';

const doc = new InputFile(bytes, 'rapport.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });

// les fichiers imbriqués dans les paramètres fonctionnent aussi — par exemple, un album :
await api.sendMediaGroup({ chat_id, media: [
  { type: 'photo', media: new InputFile(a, 'a.jpg', { type: 'image/jpeg' }) },
  { type: 'photo', media: new InputFile(b, 'b.jpg', { type: 'image/jpeg' }) },
]});

Les octets sont diffusés en continu vers Telegram sous forme de véritable téléversement multipart — le fichier n’est pas mis en tampon entier dans un seul appel à la plateforme — mais les limites de l’API Bot de Telegram s’appliquent toujours : un bot peut actuellement téléverser jusqu’à 50 Mo par fichier (et 10 Mo pour une photo).

Télécharger — par file_id. Deux assistants récupèrent les octets d’un fichier. La plateforme résout le file_id pour vous (elle exécute getFile et récupère le contenu en coulisses), vous n’interagissez donc jamais avec le jeton du bot ou une URL de téléchargement :

const bytes = await api.getFileContent(file_id);          // → Uint8Array (le fichier entier)

const { file, body } = await api.getFileStream(file_id);  // diffusez-le à la place
file.file_unique_id;                                      // les informations getFile
for await (const chunk of body) { /* Uint8Array */ }      // ou mettez en tampon : await stream.bytes()

Les téléchargements getFile de Telegram sont limités à 20 Mo, c’est donc le plafond pour les deux assistants.

HTTP

fetch est un client similaire à fetch pour appeler le monde extérieur — API tierces, webhooks, tout ce qui passe par HTTP.

import { fetch } from 'sdk';

const res = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Pavel' }) ,
});
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();

La réponse reflète la plateforme web : res.status, res.statusText, res.ok (vrai pour 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()), et les lecteurs de corps await res.json() / await res.text().

Vous pouvez également lire le corps de manière incrémentielle en tant que flux — for await (const chunk of res.body) { … } — c’est ainsi que vous consommez des événements envoyés par le serveur ou des sorties token par token des API d’IA.

Les assistants de corps définissent le Content-Type correspondant pour vous :

await fetch(url, { method: 'POST', body: fetch.body.json({ a: 1 }) }); // application/json
await fetch(url, { method: 'POST', body: fetch.body.form({ a: 1 }) }); // x-www-form-urlencoded
await fetch(url, { method: 'POST', body: fetch.body.text('salut') });     // text/plain

Les corps de requête peuvent également transporter des fichiers et des octets bruts : passez un Uint8Array, un InputFile, ou un FormData (pour multipart/form-data) comme body, et les octets sont diffusés en continu — vous pouvez donc téléverser vers une API tierce de la même manière que vous envoyez des fichiers à Telegram :

import { fetch, FormData, InputFile } from 'sdk';

const form = new FormData();
form.append('file', new InputFile(bytes, 'photo.jpg', { type: 'image/jpeg' }));
form.append('caption', 'de mon bot');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });

Il se comporte sinon comme le fetch standard que vous connaissez déjà, avec deux contraintes :

  • La réponse est lue comme du texte — await res.json() / await res.text(), ou diffusée morceau par morceau avec res.body.
  • La réponse totale est limitée à 30 Mo. Ce plafond couvre toute la réponse — le streaming avec res.body vous permet de traiter un grand corps de manière incrémentielle, mais il n’augmente pas la limite.

Journalisation

La globale standard console est disponible — rien à importer, elle est juste là comme dans n’importe quel JavaScript. Sa sortie est capturée et affichée par npx tgcloud run, ce qui en fait votre outil de débogage principal pendant le développement.

console.log('traitement', { chatId: id });   // log / debug — normal
console.info('démarré');                     // info  — bleu
console.warn('limité en débit');                // avertissement  — jaune
console.error(err);                          // erreur — rouge, avec une trace de pile

Chaque ligne est étiquetée avec le [fichier:ligne] d’où elle provient. console.error et console.trace ajoutent une pile complète, tandis que console.warn ne le fait pas. Lorsque vous npx tgcloud run un module, ces lignes sont imprimées avec un préfixe coloré par niveau, le temps écoulé depuis le début de l’exécution, et l’origine — voir run .

Interface de ligne de commande

tgcloud est le pont entre votre dossier projet et le cloud. Il initialise les projets, vous montre ce qui a changé, déploie, exécute des modules et applique des migrations de base de données. Il nécessite Node.js 18 ou plus récent. Deux façons de l’obtenir :

# Recommandé — créez un projet avec le CLI installé dedans :
npm create @tgcloud/bot example_bot

# Ou installez le CLI globalement et initialisez un dossier vide :
npm install -g @tgcloud/cli
tgcloud init

Le paquet npm est @tgcloud/cli, et la commande qu’il installe est tgcloud. Chaque commande fonctionne depuis n’importe quel sous-dossier de votre projet.

Commande But
init Initialise un nouveau projet dans le dossier actuel
add Initialise un nouveau module (un gestionnaire, un point d’accès ou un module lib)
login Lie le projet à un bot (enregistre le jeton)
status Affiche ce qui a changé localement par rapport au cloud
diff Affiche les changements ligne par ligne
push Déploie les modules modifiés et le build du Mini App dans le cloud
migrate Applique les changements de schéma à la base de données
run Exécute un module sur la plateforme sans déployer
fetch Vérifie ce qui est déployé, sans toucher aux fichiers locaux
pull Ramène les fichiers locaux dans la ligne du cloud
reset Abandonne les changements locaux ; restaure depuis l’état du cloud
webhook Inspecte et resynchronise le webhook géré par la plateforme
completion Affiche un script de complétion pour le shell (bash/zsh/fish)
upgrade Déplace un projet pré-0.2.0 dans tgcloud/

Authentification

Un projet est lié à un bot par son jeton, qui a la forme app<id>:<secret>. La partie app<id> est publique et peut être imprimée ; le secret n’apparaît jamais dans les journaux ou les erreurs.

Le jeton est résolu dans cet ordre :

  1. Variable d’environnement TGCLOUD_TOKEN — pour la CI ; jamais écrite sur disque.
  2. .tgcloud/credentials — écrit par npx tgcloud login .
  3. Aucun des deux → une erreur vous orientant vers npx tgcloud login.

Le CLI ne demande jamais de jeton en cours de commande. Se connecter est toujours l’étape explicite login. Si un jeton enregistré devient invalide, le CLI le supprime et vous demande de vous reconnecter (login) à nouveau.

init

npx tgcloud init

Initialise un nouveau projet dans le répertoire actuel : le dossier tgcloud/ avec schema.js, lib/, handlers/ et un gestionnaire de démarrage, plus tgcloud.jsonc, AGENTS.md, docs/, et le dossier d’état .tgcloud/. Exécutez-le dans la racine d’un projet front-end existant — il ajoute le dossier tgcloud/ et la documentation, et ne surcharge jamais vos fichiers. S’il reconnaît le front-end, il pointe également tgcloud.jsonc vers le dossier de build et ajoute un script deploy (build, puis push) à votre package.json — uniquement si vous n’avez pas déjà de script deploy. Voir Front-end Mini App .

init refuse de créer un projet à l’intérieur d’un autre projet. Le réexécuter dans la racine d’un projet est correct et ajoute simplement tout ce qui manque.

add

npx tgcloud add <cible>

Initialise un nouveau module unique, câblé et prêt à être édité — un gestionnaire, un point d’accès ou un module lib/.

npx tgcloud add handlers/callback_query   # un nouveau gestionnaire de mise à jour
npx tgcloud add endpoints/getProfile      # un nouveau point d'accès pour le Mini App
npx tgcloud add lib/cart                  # un nouveau module partagé

Notez que add ne surcharge jamais un fichier existant.

La <cible> est le chemin du module (un .js final facultatif). Pour handlers/, le nom doit être un type de mise à jour Telegram. La plateforme annonce l’ensemble valide, donc un nom invalide est rejeté immédiatement. handlers/ et endpoints/ sont plats, tandis que lib/ peut être imbriqué (lib/payments/stripe).

Le nom du module est obligatoire. Donner juste le répertoire est une erreur — mais une erreur utile : pour handlers/ il liste les types de mise à jour que vous n’avez pas encore, vous pouvez donc en copier un.

$ npx tgcloud add handlers
Error: Specify a name, e.g. "handlers/callback_query". Available handlers/ types: callback_query, inline_query, chat_member, …

La complétion par <Tab> propose le même ensemble — voir completion .

Le fichier généré a un export default actif, donc le gestionnaire est activé dès que vous déployez — il n’y a rien à décommenter. Déployez le nouveau module avec push .

login

npx tgcloud login

Demande votre jeton d’accès CLI — de @BotFather → votre bot → Serverless → CLI Access → Access token, un jeton distinct de votre jeton d’API de bot — le valide auprès de la plateforme, et l’enregistre dans .tgcloud/credentials.

login est la seule commande qui demande un jeton, et elle nécessite un terminal interactif. En CI, définissez TGCLOUD_TOKEN à la place.

status

npx tgcloud status

Affiche, par fichier, ce qui a changé entre votre répertoire de travail et la copie déployée : modifié, nouveau, supprimé, inchangé. Il fonctionne hors ligne, en comparant avec l’état du cloud à partir de votre dernier push, pull ou fetch. Il avertit également des fichiers .js errants dans tgcloud/ qui ne seront pas déployés.

Pour un projet avec un Mini App, status affiche également ce qui a changé dans son dossier de build depuis le dernier déploiement.

diff

npx tgcloud diff

Comme status, mais affiche les différences ligne par ligne réelles pour les modules modifiés. Également hors ligne.

push

npx tgcloud push [fichiers...]

Déploie votre projet dans le cloud en un seul lot atomique.

Sans arguments, il déploie l’ensemble du projet, et l’état déployé est fait pour refléter exactement votre dossier — les modules que vous avez supprimés localement sont supprimés dans le cloud.

Avec des arguments de fichier ou de répertoire (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/) , il déploie uniquement les changements dans ces fichiers. Une poussée ciblée ne supprime pas d’autres modules. Une cible peut être orthographiée comme le chemin du fichier (tgcloud/handlers/message.js) , le chemin à l’intérieur de tgcloud/ (handlers/message.js) ou le nom du module (handlers/message) — ils signifient tous le même module.

Sa seule option est --force — pour ignorer la vérification de concurrence et écraser tout ce qui se trouve dans le cloud. Utilisez-la uniquement lorsque vous êtes sûr (voir Rester synchronisé ).

Si tgcloud.jsonc nomme un dossier statique, un push complet le déploie dans le même lot — seuls les fichiers qui ont changé sont téléversés — et imprime l’adresse du site. Construisez d’abord, car push n’exécute jamais votre build (npm run deploy fait les deux). npx tgcloud push dist/ déploie les fichiers statiques seuls, et une poussée qui ne nomme que des modules les laisse inchangés.

Après un déploiement, si schema.js a changé et que la base de données est désynchronisée, push imprime un résumé des changements en attente et suggère npx tgcloud migrate. Il ne les applique jamais lui-même.

migrate

npx tgcloud migrate

Applique vos changements de schéma à la base de données. Il calcule la différence entre schema.js et la base de données en direct, puis vous guide étape par étape avec un compteur [N/M] en cours :

  • Un résumé bref de tous les changements en attente.
  • Changements sûrs, appliqués ensemble en une seule étape, après confirmation.
  • Avertissements (suppressions, opérations lentes), un par un, chacun confirmé séparément.
  • Changements manuels, présentés avec une raison et une action suggérée, non appliqués automatiquement.
  • Objets non documentés (dans la base de données mais pas dans votre schéma), présentés pour information, non appliqués.

Cela se termine par un résumé : appliqués, ignorés, en attente de correction manuelle, non dans le schéma.

Voir Migrations pour le modèle.

Options : --dry-run (imprimer tout, n’appliquer rien), --safe (appliquer automatiquement les changements sûrs, ignorer les avertissements), --yes (appliquer automatiquement les changements sûrs et tous les avertissements, ignorer les manuels — à utiliser avec précaution), --local (différencier par rapport à votre schema.js local plutôt qu’à celui déployé). Sans drapeau, migrate nécessite un terminal et échoue dans un environnement non interactif plutôt que de deviner.

run

npx tgcloud run <module> [args] [--ctx <json5>]

Exécute un gestionnaire ou un point d’accès sur la plateforme sans déployer, en utilisant vos fichiers locaux actuels. C’est la boucle intérieure rapide pour tester la logique.

  • <module> — un nom simple (recherché sous handlers/ et endpoints/) ou un chemin comme handlers/message. Un nom simple qui existe dans les deux répertoires doit être orthographié avec son répertoire.
  • [args] — la charge utile passée au gestionnaire, écrite en JSON5 pour que vous puissiez omettre les guillemets des clés. C’est l’objet de type de mise à jour que votre gestionnaire reçoit (par exemple, un Message pour handlers/message), ou l’input d’un point d’accès, et cela doit être un objet — pas un tableau ou une chaîne simple.
  • --ctx <json5> — l’objet de contexte du module (son deuxième argument), également en JSON5. Utilisez-le pour fournir ce que votre code lit à partir de ctx — la mise à jour brute pour un gestionnaire, --ctx '{ update: { update_id: 1 } }', ou les données d’initialisation pour un point d’accès, --ctx '{ initData: { user: { id: 1 } } }'. run ne le vérifie pas, puisque vous testez votre logique, pas les vérifications de la plateforme.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "salut" }'

La plateforme exécute le module sur l’espace de modules assemblé à partir de votre projet local (donc le code lib/ modifié localement est également utilisé) et retourne la valeur de retour, tout ce qui a été enregistré avec console.*, et le temps écoulé. Lisez de grands arguments à partir d’un fichier avec npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Vérifie ce qui est actuellement déployé, sans toucher à vos fichiers de travail. Après cela, status et diff se comparent au dernier état du cloud. Utile pour examiner un conflit avant de décider comment le résoudre.

pull

npx tgcloud pull

Ramène votre projet local dans la ligne du cloud en mettant à jour vos fichiers de travail avec l’état déployé.

pull n’écrit jamais le dossier de build de votre Mini App.

reset

npx tgcloud reset

Abandonne vos changements locaux et restaure les modules à partir du dernier état connu du cloud. Utilisez-le pour jeter une expérience. Le dossier de build du Mini App n’est pas touché.

webhook

npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]

Telegram délivre les mises à jour à votre bot via un webhook, que la plateforme gère pour vous — vous ne le pointez jamais manuellement. npx tgcloud webhook affiche son état actuel : l’URL, la liste allowed_updates, combien de mises à jour sont en attente, la dernière erreur de livraison (le cas échéant), et s’il est synchronisé avec vos gestionnaires déployés.

“Synchronisé” signifie que le webhook pointe vers la plateforme et que sa liste allowed_updates correspond aux gestionnaires que vous avez déployés — ainsi, Telegram ne délivre que les types de mise à jour que vous gérez, et rien d’autre. Déployer un nouveau gestionnaire (ou en supprimer un) peut laisser le webhook désynchronisé jusqu’à ce qu’il soit rafraîchi ; npx tgcloud status signale cela aussi.

npx tgcloud webhook sync le corrige — il re-pointe le webhook vers la plateforme et reconstruit allowed_updates à partir de vos gestionnaires déployés. Ajoutez --drop-pending pour ignorer les mises à jour que Telegram avait déjà mises en file d’attente avant la synchronisation (sinon elles seront livrées une fois le webhook à nouveau opérationnel).

completion

Note : la complétion par tabulation ne fonctionne que lorsqu’un simple tgcloud est dans votre PATH — installez-le donc globalement (npm install -g @tgcloud/cli) ou mettez autrement le binaire dans votre PATH. Il ne peut pas s’intégrer à npx.

tgcloud completion <bash|zsh|fish>

Affiche un script de complétion pour le shell sur stdout. Activez-le une fois, puis <Tab> complétera les commandes, les drapeaux, les répertoires de modules, les types de mise à jour de gestionnaires dont vous n’avez pas encore besoin, et vos modules exécutables locaux.

# bash — nécessite le paquet bash-completion :
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — assurez-vous que `autoload -U compinit && compinit` s'exécute dans votre ~/.zshrc :
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

# fish :
tgcloud completion fish > ~/.config/fish/completions/tgcloud.fish

Redémarrez votre shell (ou re-sourcez le fichier) ensuite. Exécuter tgcloud completion sans shell affiche à nouveau ces instructions.

upgrade

npx tgcloud upgrade [--dry-run]

Migration unique pour les projets créés avant le CLI 0.2.0, qui gardaient schema.js, lib/ et handlers/ à la racine du projet. Il les déplace dans tgcloud/. Votre lien avec le bot est conservé, et rien n’a besoin d’être redéployé. --dry-run imprime les déplacements prévus sans toucher à quoi que ce soit. Les autres commandes ne fonctionnent pas sur l’ancien agencement tant que vous n’avez pas effectué la mise à niveau.

Rester synchronisé

Chaque projet a une révision augmentant de manière monotone dans le cloud, incrémentée à chaque déploiement. Le CLI se souvient de la révision avec laquelle il s’est synchronisé en dernier et l’envoie à chaque push. Si le cloud a avancé — parce qu’une autre machine ou un collègue a déployé — le push est rejeté au lieu d’écraser silencieusement leur travail, et le CLI propose trois solutions :

npx tgcloud fetch           # voir ce qui a changé dans le cloud, puis re-vérifier
npx tgcloud pull            # ramener le dernier état dans vos fichiers de travail
npx tgcloud push --force    # écraser l'état du cloud (dangereux)

Cette vérification de concurrence optimiste est la raison pour laquelle vous pouvez partager un bot entre une équipe sans processus de déploiement figé. Les commandes se terminent avec un code non nul en cas d’échec — un déploiement rejeté, une migration échouée, une erreur d’authentification, un module qui a lancé une exception pendant run — elles se composent donc proprement dans les scripts et les pipelines CI.