Telegram Serverless ti permette di eseguire codice backend per il tuo bot e Mini App direttamente sull’infrastruttura di Telegram: niente server da gestire, nessun container da tenere attivo, nessuna scalabilità a cui pensare. Scrivi moduli JavaScript semplici, li distribuisci con un singolo comando e Telegram li esegue in una sandbox V8 veloce e isolata, situata proprio accanto alla Bot API e a un database integrato.

Se hai mai collegato un bot a un VPS, una funzione cloud o un pannello di hosting solo per rispondere a un comando /start, questa è la parte che non dovrai più fare.

Perché serverless

Un bot Telegram è, nel suo cuore, un programma che reagisce agli aggiornamenti. Tradizionalmente dovevi ospitare quel programma da qualche parte che fosse sempre attivo, raggiungibile e sicuro, e poi mantenerlo in quello stato. Telegram Serverless rimuove completamente questo livello:

  • Nessuna infrastruttura. Non ci sono macchine da noleggiare, aggiornare o monitorare. Il tuo codice viene eseguito su richiesta e scala automaticamente con il tuo bot.
  • Credenziali incluse. La Telegram Bot API, un database basato su SQLite e l’HTTP in uscita sono disponibili per ogni modulo fin da subito: nulla da installare, nessuna credenziale da configurare.
  • Esecuzione veloce e isolata. Ogni invocazione viene eseguita in un isolamento V8 leggero, vicino ai sistemi di Telegram, quindi le chiamate alla Bot API e al tuo database sono rapide e affidabili.
  • Un vero flusso di lavoro per sviluppatori. Un progetto vive in una cartella sul tuo computer sotto controllo di versione. Modifichi i file, vedi esattamente cosa è cambiato, distribuisci in modo atomico e aggiorni lo schema del database con migrazioni revisionate: il modo in cui lavori già con tutto il resto.

Il modello mentale: Come funziona il progetto

Lavori in tre posti, che si mappano chiaramente l’uno sull’altro:

Dove Cosa contiene
La tua cartella progetto Moduli JavaScript — schema, codice condiviso, gestori di aggiornamento, endpoint Mini App — e, se ne hai uno, il front‑end della Mini App
Il cloud La copia distribuita di quei moduli, il database del tuo bot e i file della Mini App
La CLI tgcloud Il ponte: ti mostra le differenze e le sincronizza

Non usi mai SSH per connetterti a nulla. Modifichi i file localmente, esegui npx tgcloud push e la piattaforma si occupa del resto. Il traffico del tuo bot viene gestito dai moduli distribuiti; il tuo database persiste tra le invocazioni.

Un progetto ha solo quattro tipi di codice, tutti all’interno di una singola cartella tgcloud/:

tgcloud/
├─ endpoints/  # funzioni chiamate dalla tua Mini App
├─ handlers/   # funzioni che reagiscono ai tipi di aggiornamento di Telegram
├─ lib/        # codice condiviso che importi da ovunque
└─ schema.js   # le tabelle del tuo database

Quando arriva un aggiornamento — un messaggio, la pressione di un pulsante, una inline query — Telegram lo instrada al gestore corrispondente (handlers/message.js, handlers/callback_query.js, …) e chiama la sua esportazione predefinita. Quella funzione comunica con la Bot API e il database tramite l’SDK e termina. Questo è l’intero ciclo. Un aggiornamento senza un gestore corrispondente viene semplicemente ignorato, quindi aggiungi solo i gestori di cui hai bisogno.

Una Mini App è facoltativa. Se il tuo bot ne ha una, il suo front‑end vive nello stesso progetto, accanto a tgcloud/. Serverless ospita il suo output di compilazione ed esegue gli endpoints/ che chiama. Vedi Front‑end della Mini App .

Demo rapida

Ecco un bot demo completo e funzionante. Risponde a ogni messaggio e ricorda quanti ne ha visti da ogni 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;

  // Inserisci il contatore, o incrementalo se questa chat ne ha già uno — e ottieni
  // la riga risultante nello stesso statement tramite .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: `Ciao! Ho visto ${row.seen} messaggio/i da parte tua.`,
  });
}

Distribuiscilo:

npx tgcloud push       # carica i moduli
npx tgcloud migrate    # crea la tabella `counters`

Questo è un bot attivo con stato persistente e senza server. Tutto al suo interno — api, db, il DSL table() — è descritto nelle sezioni seguenti.

Serverless è un backend generale per bot Telegram e Mini App, non un modello per un solo tipo di applicazione. È ideale per:

  • Bot di AI conversazionale che hanno bisogno di memorizzare lo stato per utente in un database.
  • Backend di Mini App che memorizzano dati utente e servono contenuti dinamici.
  • Giochi e Strumenti — inclusi classifiche, quiz e altro.
  • Automazioni e Integrazioni che chiamano API HTTP di terze parti e inviano i risultati nelle chat.

Iniziare

Questa guida ti porta da una cartella vuota a un bot attivo che risponde ai messaggi e memorizza i dati. Si presume che tu abbia Node.js 18 o superiore installato e un bot registrato presso @BotFather . Alla fine avrai usato ogni comando necessario quotidianamente: push, migrate, run e status.

Prima di tutto, attiva Serverless. In @BotFather , apri il tuo bot → Serverless e attivalo. Questo abilita la funzionalità per questo bot e sblocca il suo token di accesso alla CLI, i gestori, la libreria e il database.

1. Crea un progetto

Il modo più veloce per iniziare è usare il creatore di progetti, che prepara un progetto e vi installa la CLI:

npm create @tgcloud/bot example_bot
cd example_bot

L’argomento è la cartella di destinazione: passa . per creare il progetto nella cartella corrente, o qualsiasi percorso. Funziona anche in una cartella esistente e non sovrascrive mai i file che già possiedi.

Questo ti fornisce un progetto pronto per essere modificato:

example_bot/
├─ tgcloud/             # tutto ciò che la piattaforma esegue
│  ├─ handlers/
│  │  └─ message.js     # un gestore di messaggi iniziale (risponde ripetendo il testo)
│  ├─ lib/              # i tuoi moduli condivisi vanno qui (vuoto all'inizio)
│  └─ schema.js         # le tabelle del tuo database
├─ docs/
│  └─ tgcloud-sdk.md    # riferimento SDK (per te e i tuoi strumenti AI)
├─ AGENTS.md            # orientamento per assistenti di codifica AI
├─ package.json
└─ tgcloud.jsonc        # configurazione del progetto

I file creati contengono istruzioni al loro interno: ognuno include esempi commentati su cosa fare dopo.

La CLI si installa nel progetto come dipendenza di sviluppo locale, quindi la esegui con npx tgcloud <comando> (npx trova la copia nella cartella node_modules del tuo progetto), o tramite le scorciatoie npm run che lo scaffold aggiunge a package.json (npm run deploy, npm run status). Per impostazione predefinita, non esiste un tgcloud globale nel tuo PATH.

Puoi anche installarlo globalmente — npm install -g @tgcloud/cli — se preferisci digitare un semplice tgcloud da qualsiasi luogo. È comodo per eseguire tgcloud init in qualsiasi cartella vuota, ed è ciò di cui ha bisogno il completamento automatico della shell. In entrambi i casi ottieni lo stesso progetto.

Stai costruendo una Mini App? Inizia dal suo front‑end. Crealo con il tuo strumento abituale, poi esegui il creatore di progetti in quella cartella. Vedi Front‑end della Mini App .

2. Collega il tuo bot

Ogni progetto è legato a un bot. Collegali con login, che richiede il tuo token di accesso alla CLI ( @BotFather → il tuo bot → Serverless → CLI Access → Access token — un token separato dal token API del tuo bot ) e lo memorizza localmente:

npx tgcloud login

Il token ha la forma app<id>:<secret>. La CLI lo mantiene in .tgcloud/, che è ignorato da git, e non stampa mai la parte segreta. Il login è l’unica volta in cui ti viene chiesto — vedi Autenticazione per come i token vengono risolti in CI.

3. Dai un’occhiata

Due comandi ti dicono dove stanno le cose in qualsiasi momento, entrambi completamente offline:

npx tgcloud status     # cosa è cambiato localmente rispetto alla copia distribuita
npx tgcloud diff       # le modifiche riga per riga

Subito dopo init tutto è nuovo e nulla è ancora distribuito. status mostra i file iniziali in attesa di essere caricati.

4. Distribuisci

Invia i tuoi moduli al cloud:

npx tgcloud push

push carica ogni modulo modificato in un unico batch atomico e aggiorna il tuo registro locale di ciò che il cloud contiene ora. Il tuo bot è attivo: aprilo su Telegram e inviagli un messaggio — il gestore iniziale ti risponderà ripetendo il testo.

La distribuzione non tocca mai il tuo database. Caricare il codice e modificare lo schema del database sono passaggi deliberatamente separati, così una distribuzione di codice non può mai sorprenderti con una migrazione di dati. A questo serve il passaggio successivo.

5. Aggiungi una tabella al database

Facciamo in modo che il bot ricordi qualcosa. Apri tgcloud/schema.js e dichiara una tabella:

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())`) ,
});

Distribuisci lo schema, poi applicalo al database:

npx tgcloud push       # carica il nuovo schema
npx tgcloud migrate    # crea la tabella `messages`

push segnala che lo schema non è sincronizzato e ti mostra la modifica in sospeso, ma non applica nulla. migrate ti guida attraverso la modifica e, alla tua conferma, crea la tabella. Questo modello a due passaggi — e cosa succede con modifiche più rischiose come le eliminazioni — è trattato in Migrazioni .

6. Memorizza e leggi i dati

Ora usa la tabella dal tuo gestore. Modifica tgcloud/handlers/message.js:

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

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

  // Conta quanti ne abbiamo memorizzati per questa chat.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Salvato. Sono ${count} messaggio/i da questa chat finora.`,
  });
}

Distribuisci il gestore aggiornato con npx tgcloud push, poi invia al tuo bot alcuni messaggi e guarda il conteggio salire. Il database persiste tra le invocazioni — questa è la memoria del tuo bot.

7. Testa senza distribuire

Non devi distribuire per provare una modifica. npx tgcloud run esegue un gestore sulla piattaforma utilizzando i tuoi file locali, senza pubblicarli:

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

L’argomento è il payload che riceve il tuo gestore — per handlers/message, un Messaggio — scritto in JSON5 (così puoi evitare di mettere le virgolette alle chiavi). Il comando stampa tutto ciò che il gestore ha registrato con console.*, il valore di ritorno e quanto tempo ha impiegato. Questo è il ciclo più rapido per iterare sulla logica — niente distribuzione, niente attesa per un messaggio reale.

8. Mantieni la sincronizzazione

Mentre lavori, una manciata di comandi mantiene allineati il tuo progetto locale e il cloud: npx tgcloud status mostra cosa è cambiato, npx tgcloud push distribuisce, npx tgcloud pull allinea il tuo progetto locale con il cloud, npx tgcloud fetch controlla cosa è distribuito senza toccare i tuoi file e npx tgcloud reset scarta le modifiche locali.

Se due persone (o due macchine) distribuiscono sullo stesso bot, la piattaforma rileva il conflitto e push si ferma per permetterti di fare prima pull — non puoi mai sovrascrivere silenziosamente il lavoro di qualcun altro. Vedi Mantenersi sincronizzati .

Costruire con l’IA

Preferisci costruire con un assistente IA — o l’unico programmatore nel tuo team è un’IA? Puoi ancora pubblicare un bot. Abbiamo fatto un primo passo per far sentire un agente IA a casa in un progetto: ogni nuovo progetto viene creato con un AGENTS.md e un riferimento docs/tgcloud-sdk.md che gli strumenti di codifica agentica leggono automaticamente.

Insieme a un runtime piccolo e autonomo — un SDK, nessun pacchetto npm da gestire — questo dà all’assistente un vantaggio sulle convenzioni che la generazione di codice generica tende a mancare qui: importazioni relative con .js, nessuna chiave esterna, ogni chiamata db è asincrona, un gestore per tipo di aggiornamento e il flusso in due passaggi push/migrate.

Provaci:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # o Claude Code, Cursor, … — qualsiasi agente che legga AGENTS.md

Poi chiedi semplicemente, in linguaggio naturale:

Scrivi un bot che ricordi la lista delle cose da fare di ogni persona — aggiungi un elemento quando inviano un testo e mostra l’intera lista quando inviano /list.

L’assistente modifica schema.js e i tuoi gestori per te; tu revisioni, testi una modifica istantaneamente con npx tgcloud run, poi vai live con npx tgcloud push e npx tgcloud migrate. AGENTS.md fa parte del tuo progetto — modificalo man mano che il bot cresce affinché la guida rimanga accurata.

In movimento con BotFather

Sei solo con il tuo telefono? L’intero progetto vive anche su @BotFather — apri il tuo bot → Serverless e ottieni tutto ciò che la CLI gestisce, su un touchscreen:

  • Gestori — crea, modifica ed esegui test dei gestori di aggiornamento; BotFather mantiene il webhook sincronizzato con i gestori che hai (lo stesso In sync / Out of sync segnalato dalla CLI).
  • Endpoint — le funzioni che chiama la tua Mini App.
  • Libreria — i tuoi moduli lib/ condivisi.
  • Database — modifica schema.js con la stessa sintassi stile Drizzle, rivedi le modifiche in sospeso e applicale; Salva distribuisce.
  • File della Mini App — il front‑end che i tuoi utenti aprono su Telegram.
  • Accesso CLI — prendi qui il token di accesso alla CLI quando torni a una tastiera.

È lo stesso progetto cloud, quindi puoi iniziare un gestore sul tuo telefono e fare npx tgcloud pull sul tuo laptop in seguito — nulla è legato a un singolo client. L’esecuzione di un gestore mostra persino il suo output di Console direttamente nella chat, proprio come npx tgcloud run .

Progetti e moduli

Un progetto serverless è una cartella ordinaria sotto controllo di versione. Tutto ciò che la piattaforma esegue risiede in una cartella, tgcloud/; il resto del progetto è tuo — un front‑end per la mini-app, configurazione, documenti. Non c’è un passaggio di build per il bot, nessuna node_modules a runtime e nessun punto di ingresso del server.

Anatomia di un progetto

example_bot/
├─ tgcloud/             # la parte distribuita — nient'altro lo è
│  ├─ handlers/         # gestori di aggiornamento — piatti, solo un livello
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # funzioni che chiama la tua Mini App — piatte, solo un livello
│  │  └─ getProfile.js
│  ├─ lib/              # moduli condivisi; sottodirectory consentite
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # schema del database — un file
└─ .tgcloud/            # stato CLI, ignorato da git

Solo i file .js all’interno di tgcloud/ — schema.js, lib/, handlers/, endpoints/ — vengono distribuiti. Tutto il resto — Markdown, file di configurazione, la cartella .tgcloud/, qualsiasi cosa fuori da tgcloud/ — rimane sul tuo computer. I nomi dei moduli sono relativi a tgcloud/. Il file tgcloud/handlers/message.js è il modulo handlers/message, ed è il nome che prende npx tgcloud run.

Aggiorni da un progetto più vecchio? I progetti creati prima della CLI 0.2.0 mantenevano schema.js, lib/ e handlers/ alla radice del progetto. Esegui npx tgcloud upgrade una volta per spostarli in tgcloud/.

schema.js — il tuo database. Dichiara le tabelle come esportazioni denominate usando il DSL dello schema e risiede in tgcloud/ come file singolo. Viene distribuito come qualsiasi altro modulo, ma distribuirlo non cambia mai il database — le modifiche allo schema vengono applicate separatamente con npx tgcloud migrate. Vedi Il database .

lib/ — codice condiviso, tutto ciò che vuoi riutilizzare tra i gestori: helper puri, livelli di accesso al database, formattazione, integrazioni con servizi esterni. lib/ è l’unica directory che può contenere sottodirectory (lib/internal/util.js, lib/payments/stripe.js), quindi puoi organizzare una base di codice più grande come preferisci. I moduli in lib/ non vengono mai invocati direttamente dalla piattaforma; esistono per essere importati dai gestori e tra loro.

handlers/ — i punti di ingresso del tuo bot. Ogni file corrisponde a un tipo di aggiornamento Telegram, e la piattaforma instrada ogni aggiornamento in arrivo al gestore corrispondente:

File Gestisce
handlers/message.js Nuovi messaggi in arrivo
handlers/inline_query.js Nuove query inline in arrivo
handlers/callback_query.js Nuove callback query in arrivo
… qualsiasi altro tipo di aggiornamento Bot API

handlers/ è piatto — niente sottodirectory. L’ export default di un gestore è la funzione chiamata dalla piattaforma (vedi Gestori ).

Un tipo di aggiornamento viene gestito solo se il file del gestore esiste e non è vuoto. Se non c’è handlers/<tipo>.js — o il file è vuoto — gli aggiornamenti di quel tipo vengono ignorati e la piattaforma non esegue nulla per essi. Quindi tieni solo i gestori di cui hai effettivamente bisogno: ogni gestore che aggiungi è un altro tipo di aggiornamento che il tuo bot deve elaborare, e lasciare fuori gli altri significa che Telegram non avvia il tuo codice per aggiornamenti che altrimenti scarteresti comunque.

Per creare un nuovo gestore, esegui npx tgcloud add handlers/<tipo> .

endpoints/ — il lato server della tua Mini App. Ogni file è una funzione che la Mini App chiama per nome, quindi endpoints/getProfile.js risponde a POST /api/getProfile. La piattaforma verifica che la chiamata provenga da un utente Telegram all’interno della tua Mini App prima che il tuo codice venga eseguito, e ti passa quell’utente. endpoints/ è piatto, come handlers/. Vedi Endpoint .

.tgcloud/ — stato locale della macchina gestito interamente dalla CLI, incluse le tue credenziali salvate. È ignorato da git e non dovresti mai leggerlo o scriverlo a mano — usa invece i comandi della CLI.

Il sistema dei moduli

A runtime, un modulo può vedere esattamente due cose: l’SDK della piattaforma e gli altri moduli nel tuo progetto. Non ci sono pacchetti npm, nessun filesystem e nessuna rete tranne tramite il fetch dell’SDK.

Importa i tuoi moduli come faresti in qualsiasi progetto JavaScript — tramite percorso relativo, con l’estensione .js — e l’SDK per il suo nome:

// tgcloud/handlers/message.js
import { users } from '../schema.js';              // il modulo schema
import { addItem } from '../lib/cart.js';          // un modulo lib
import { format } from '../lib/internal/fmt.js';  // modulo lib annidato
import { db, api, fetch } from 'sdk';              // l'SDK della piattaforma
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        // un vicino
import { users } from '../schema.js';

Un’importazione relativa deve terminare in .js e non può portare fuori da tgcloud/:

import { users } from '../schema';       // aggiungi .js
import config from '../../config.js';    // fuori da tgcloud/
import { z } from 'zod';                 // niente pacchetti npm

Ogni modulo ha anche un nome — il suo percorso all’interno di tgcloud/ senza estensione, come schema, lib/cart o handlers/message. Quel nome è ciò che usano npx tgcloud run e l’output della CLI, e un modulo può essere importato anche tramite esso (from 'lib/cart', nessuna estensione). Preferisci le importazioni relative nel tuo codice. Sono ciò che il tuo editor comprende — vai alla definizione, completamento automatico, rinomina.

Ciò che è disponibile a runtime sono esattamente due cose: sdk e i suoi sottomoduli (sdk/db, sdk/api, sdk/fetch) — l’intera superficie della piattaforma, vedi L’SDK — e i tuoi moduli in tgcloud/. Questo è l’elenco completo. Se il tuo codice importa qualsiasi altra cosa, non verrà risolto. Questo vincolo è ciò che mantiene i moduli veloci da caricare e sicuri da eseguire.

Gestori

Un gestore è un modulo in handlers/ la cui esportazione predefinita viene invocata dalla piattaforma quando arriva un aggiornamento corrispondente.

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Hai detto: ${message.text ?? '(nessun testo)'}`,
  });
}

Un gestore riceve il payload dell’aggiornamento come argomento — la piattaforma scarta l’ Update di Telegram per te. handlers/message.js ottiene il Messaggio (es. update.message); handlers/callback_query.js ottiene la CallbackQuery ; e così via. Il secondo argomento del gestore è un oggetto contesto per invocazione, ctx. Trasporta l’ Update grezzo come ctx.update — usalo quando hai bisogno di qualcosa al di fuori del payload, come update_id.

Un gestore può essere async (di solito lo è) e può restituire un valore. Raggiunge la Bot API, il database e l’HTTP in uscita tramite l’SDK.

Non hai bisogno di un aggiornamento reale per testare un gestore. npx tgcloud run lo esegue sulla piattaforma con il payload che fornisci e il tuo codice locale corrente:

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

L’argomento è il payload — lo stesso oggetto che riceve il tuo gestore — in JSON5. Per fornire il ctx del gestore (il suo secondo argomento), aggiungi --ctx, es. --ctx '{ update: { update_id: 1 } }'. Questo viene eseguito contro i tuoi file locali, quindi puoi provare le modifiche prima di distribuirle. Vedi run .

Endpoint

Un endpoint è un modulo in endpoints/ che la tua Mini App chiama — il posto per tutto ciò che il front‑end non può fare da solo, come leggere e scrivere il database, chiamare la Bot API o parlare con servizi esterni con i tuoi segreti.

// 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('Profilo non trovato', { userId: user.id });
  return profile;
}

Il primo argomento, input, è l’oggetto JSON inviato dalla Mini App. Il secondo, ctx, trasporta ctx.initData — i dati init della Mini App (user, chat, start_param, auth_date, …), già verificati dalla piattaforma. Hanno la stessa forma di Telegram.WebApp.initDataUnsafe sul client, ma qui puoi fidarti. Una chiamata senza dati init validi viene rifiutata con 401 prima che il tuo codice venga eseguito, quindi ctx.initData.user è realmente la persona che ha aperto la tua Mini App. Gli endpoint possono essere chiamati solo in questo modo — dalla Mini App del tuo bot, da un utente Telegram — quindi non c’è nessuna API pubblica da proteggere e nessun login da costruire.

Ciò che l’endpoint restituisce viene rispedito come JSON. Per rifiutare una chiamata, throw new EndpointError(descrizione, parametri?). La Mini App ottiene la descrizione e l’oggetto parameters così come sono. Se hai bisogno di ramificare il tipo di errore nel front‑end, inserisci un codice in parameters — { code: 'OUT_OF_STOCK' }. Qualsiasi altra eccezione viene segnalata come errore interno generico e il messaggio reale va solo nei tuoi log, quindi i dettagli interni non trapelano mai al client.

Chiamare un endpoint dalla Mini App

La Mini App chiama un endpoint per nome con Telegram.WebApp.Serverless.call dall’SDK ufficiale per Mini App , telegram-web-app.js — lo script che la tua Mini App include già. Invia i dati init per te e richiama con un errore o il risultato dell’endpoint:

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — la descrizione; err.parameters — ciò che il tuo EndpointError trasportava
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

L’input è facoltativo e deve essere un oggetto semplice. Senza di esso la chiamata è Serverless.call('getProfile', callback).

err.type è 'ENDPOINT_ERROR' quando il tuo endpoint ha lanciato un EndpointError. Qualsiasi altro valore significa che la piattaforma ha rifiutato la chiamata o l’endpoint non è riuscito in modo imprevisto, per esempio 'UNAUTHORIZED' quando i dati init mancano o non sono validi.

I nomi degli endpoint sono come identificatori JavaScript — lettere, cifre e _, non iniziano con una cifra (getProfile, place_order). Creane uno con npx tgcloud add endpoints/<nome> , e provalo senza la Mini App con run , passando tu stesso i dati init:

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

Cosa viene distribuito

Quando fai npx tgcloud push, la CLI raccoglie ogni file .js sotto schema.js, lib/, handlers/ e endpoints/, e invia quell’esatto insieme come spazio moduli del tuo progetto — insieme alla cartella di build della Mini App, se tgcloud.jsonc ne nomina una (vedi sotto). Tutto ciò che è presente nel cloud ma assente dal tuo progetto viene rimosso, quindi lo stato distribuito riflette sempre la tua cartella — eliminazioni incluse. Nulla al di fuori di tgcloud/ viene distribuito come codice — il resto della radice del progetto è tuo. All’interno di tgcloud/, un file .js che non si trova in una di quelle posizioni (tgcloud/util.js, tgcloud/helpers/x.js) viene segnalato, in modo che non rimanga non distribuito silenziosamente. Markdown, dotfile e .tgcloud/ non vengono mai distribuiti.

Front‑end della Mini App

La radice del progetto è tua ed è il posto naturale per il front‑end della tua Mini App — costruito con React, Vue, Svelte o HTML semplice, da Vite o qualsiasi altro bundler. Serverless ospita il suo output di build accanto al tuo bot. Un push distribuisce il codice e i file statici insieme, e l’app viene servita su https://app<app_id>.tgcloud.ai/ (push stampa l’indirizzo esatto — usalo come URL della tua Mini App in @BotFather ).

Segnalare abusi. Le Mini App su tgcloud.ai sono pubblicate da sviluppatori indipendenti. Per segnalare contenuti illegali o abusi su un sottodominio app<app_id>.tgcloud.ai, invia un’email a [email protected] .

Per iniziare, crea prima il front‑end, con lo strumento che preferisci, poi aggiungi Serverless sopra di esso:

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

Il creatore di progetti non sovrascrive mai i tuoi file. Aggiunge la cartella tgcloud/ accanto al front‑end, riconosce la cartella di build e aggiunge uno script npm run deploy che costruisce l’app e la distribuisce insieme al bot.

Ciò che viene ospitato è impostato in tgcloud.jsonc, il file di configurazione del progetto nella radice:

{
  "$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 }]
  }
}

Solo "source" è richiesto. È la cartella di build, relativa alla radice del progetto. Le altre chiavi di "static" configurano come viene servito (sotto). init crea tgcloud.jsonc per te. Quando riconosce il front‑end, riempie anche "source":

Front‑end init imposta "source" a
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular niente — configura prima un’esportazione statica, poi imposta "source" tu stesso

Con qualsiasi altra configurazione, imposta "source" alla tua cartella di build tu stesso.

La riga $schema fornisce al tuo editor il completamento automatico e la validazione per ogni chiave. Il file è JSON con commenti e virgole finali consentiti. Se un tuo altro strumento deve leggerlo come JSON semplice, chiamalo tgcloud.json — allora deve essere JSON rigoroso.

Costruisci prima di fare push. push carica la cartella esattamente com’è su disco — eccetto i file e le cartelle il cui nome inizia con un punto, che vengono saltati — e non esegue mai la tua build, quindi un semplice npx tgcloud push dopo aver modificato il front‑end distribuirebbe la build precedente. Usa npm run deploy (esegue il tuo script build, poi tgcloud push), o costruisci prima tu stesso.

Le opzioni di servizio:

  • spa — serve index.html per percorsi che non esistono e non hanno estensione, così un router lato client funziona al ricaricamento. I percorsi con un’estensione ottengono comunque 404.
  • immutable — pattern per file con hash del contenuto (/assets/* di Vite) che i browser possono memorizzare nella cache per un anno. Usalo solo per file con hash. Qualsiasi altro file corrispondente qui rimarrebbe obsoleto nei browser per un anno, e non c’è modo di richiamarlo.
  • headers — intestazioni di risposta per i percorsi che una regola corrisponde (un percorso esatto, o un prefisso che termina in /*). Un valore null rimuove un’intestazione.
  • redirects — regole di reindirizzamento, dove vince la prima corrispondenza. Un file esistente batte sempre una regola. status è 301 a meno che non lo imposti.
  • "static": "dist" — un’abbreviazione per { "source": "dist" } quando non hai bisogno di altro.
  • "static": false — rimuovi il sito distribuito al prossimo push. Eliminare la chiave invece impedisce solo a questo progetto di gestire file statici, e ciò che è distribuito rimane.

Il sito viene servito dalla radice del suo host, quindi la base (/) predefinita del tuo bundler è corretta. Telegram Web apre le Mini App in un iframe, quindi non aggiungere X-Frame-Options o una policy restrittiva frame-ancestors.

Lo stesso host risponde ai tuoi endpoint sotto /api/ — è lì che va Telegram.WebApp.Serverless.call('getProfile', …). Non inserire file tuoi sotto /api/ nella cartella di build.

Il database

Ogni bot ottiene il suo database — un archivio basato su SQLite che persiste tra le invocazioni ed è disponibile per ogni modulo tramite db. Descrivi le tue tabelle in schema.js con un piccolo DSL tipizzato; le leggi e le scrivi con un costruttore di query fluido; e le evolvi con migrazioni revisionate.

Conosci Drizzle ORM ? Allora sai già come parlare con il database qui. Il DSL dello schema e il costruttore di query seguono Drizzle — i costruttori di colonne, select().from().where(), gli operatori, onConflictDoUpdate, .returning() e il tag sql si comportano tutti come ti aspetteresti, quindi leggere e scrivere dati è l’API familiare che già usi. La importi solo da sdk/db, e un paio di specifiche della piattaforma (in particolare niente chiavi esterne ) vengono indicate dove appaiono.

Dichiarare tabelle

Le tabelle sono esportazioni denominate in schema.js. Chiamare table() costruisce una descrizione al momento del caricamento (non tocca il database); la piattaforma scopre le tabelle esportate quando distribuisci schema.js e migra il database per corrispondere.

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(nome, colonne, extra?):

  • nome è il nome della tabella SQL;
  • colonne è una mappa di proprietà JS → definizione colonna;
  • extra è un callback facoltativo (t) => ({ … }) dove t espone le colonne (t.created è un riferimento a quella colonna) — dichiara indici e vincoli a livello di tabella qui.

Tipi di colonna

Factory Tipo SQLite Note
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB legge/scrive Uint8Array
boolean() INTEGER memorizzato come 0/1, letto come true/false
json() TEXT auto JSON.stringify / JSON.parse

L’argomento nome colonna è facoltativo — omettilo e viene usata la chiave JS. L’opzione mode controlla come i valori vengono convertiti tra SQLite e JavaScript.

mode Memorizzato come Valore JS
boolean INTEGER 0/1 boolean
json TEXT (JSON) qualsiasi oggetto/array
timestamp INTEGER (secondi unix) Date
timestamp_ms INTEGER (ms unix) Date
bytes BLOB Uint8Array

boolean() e json() sono abbreviazioni per integer(name, { mode: 'boolean' }) e text(name, { mode: 'json' }).

Un blob() legge e scrive un Uint8Array — il runtime non ha Buffer di Node (e un Buffer è una sottoclasse di Uint8Array, quindi questo è il tipo base portatile). Il mode sopra governa solo come un valore viene codificato, indipendentemente dal tipo di archiviazione della colonna: quindi blob('col', { mode: 'json' }) è la controparte BLOB di json() — la stessa codifica JSON, mantenuta in una colonna BLOB piuttosto che TEXT.

TLDR: blob() usa Uint8Array. mode controlla la codifica, non l’archiviazione, quindi blob(..., { mode: 'json' }) memorizza JSON come BLOB, mentre json() lo memorizza come TEXT.

Modificatori di colonna

I modificatori di colonna si concatenano a una colonna.

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' })  // o 'virtual'
text('email').deprecated('sostituito da login')   // segna la colonna per la rimozione

.default() su una colonna json() codifica il valore per te; un default sql`…` viene passato testualmente. .deprecated() è terminale — vedi Migrazioni .

Indici e vincoli

Indici e vincoli sono dichiarati nel callback extras, dove le colonne sono nell’ambito.

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})`) ,          // indice espressione
  active: index('idx_active').on(t.userId).where(sql`done = 0`) ,  // indice parziale
}));

I modificatori a livello di tabella si concatenano dopo table(...): .strict(), .withoutRowid(), .deprecated('motivo').

Niente chiavi esterne

Il runtime viene eseguito con PRAGMA foreign_keys disattivato. Una chiave esterna dichiarata sarebbe silenziosamente inerte — nessuna cascata, nessuna protezione dagli orfani — il che è peggio che non averne nessuna, quindi il DSL rende impossibile averle. Ovvero, .references() e foreignKey() a livello di tabella lanciano errori quando dichiarati, e uno schema che li usa non verrà distribuito.

Dovresti modellare le relazioni con colonne semplici (userId: integer('user_id')) e applicare l’ integrità nel codice della tua applicazione: inserisci i genitori prima dei figli, elimina i figli prima dei genitori, gestisci gli errori correttamente ed elimina gli orfani con una LEFT JOIN … WHERE parent.id IS NULL quando ne hai bisogno.

L’assenza di chiavi esterne è un vincolo deliberato, non una svista. Tienine conto fin dall’inizio quando pianifichi il tuo bot.

Interrogazione

db è un costruttore di query fluido. Ogni query è asincrona — i metodi terminali (.all(), .get(), .values(), .run()) restituiscono Promise, quindi sempre 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();                          // tutte le righe
await db.select().from(todos).where(eq(todos.id, 1)).get();   // prima riga o null
await db.select().from(todos).values();                       // righe come array di valori

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();

// proiezione personalizzata: { 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();

// conteggio righe — un helper, non un terminale di build:
await db.$count(todos);                        // tutte le righe
await db.$count(todos, eq(todos.done, false)); // con un filtro
  • Concatenabile: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Terminali: .all(), .get(), .values().
  • Conta le righe con db.$count(table, where?) (o count() in una proiezione).

Inserimento, aggiornamento, eliminazione:

await db.insert(todos).values({ userId: 1, text: 'Compra latte' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // batch
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();

Nota che un inserimento batch è un’istruzione, quindi è limitato dal limite di variabili di SQLite (righe × colonne); se lo superi, l’inserimento genera un errore — suddividilo tu stesso.

Gli operatori vengono importati da 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) con argomenti multipli è lo stesso di and(a, b). Il secondo argomento di un confronto è un valore per impostazione predefinita, ma può essere un’altra colonna o sql`…` — es. eq(a.x, b.y). Gli aggregati (count/sum/avg/min/max) sono frammenti sql per una proiezione .select({ … }).

SQL grezzo — quando il costruttore non è abbastanza, scendi allo SQL grezzo. La modalità è scelta dal metodo: db.run per scritture, db.all per molte righe, db.get per una riga, db.values per righe come array posizionali.

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, 'compra latte']]

Una scrittura risolve in un risultato di esecuzione — rowsAffected, lastInsertRowid e rows, che trasportano una clausola RETURNING se lo SQL ne aveva una. Usa db.values quando una query proietta due colonne con lo stesso nome (una join come SELECT u.id, o.id): db.all usa il nome della colonna come chiave per le righe, quindi il secondo id sovrascriverebbe il primo.

Il tag sql`…` trasforma ${value} in un parametro associato e ${table.column} in un identificatore, e concatena frammenti sql annidati. Usa sql.raw('…') per un letterale senza parametri.

Le query grezze non sono legate a una tabella, quindi le loro righe tornano senza conversione di modalità — un booleano è 0/1, una colonna JSON è una stringa, un timestamp è un numero. Solo il costruttore legato alla tabella converte i valori.

Migrazioni

Il tuo database cambia man mano che il tuo bot cresce. La piattaforma mantiene tutto al sicuro separando la distribuzione del codice dalla modifica dei dati e classificando ogni modifica dello schema in base al suo livello di rischio.

La distribuzione non tocca mai il database. Quando fai npx tgcloud push di uno schema.js modificato, la piattaforma registra il nuovo schema e ti dice cosa cambierebbe nel database — ma non applica nulla:

npx tgcloud push       # distribuisci schema.js; segnala modifiche DB in sospeso
npx tgcloud migrate    # rivedi e applicale

npx tgcloud migrate calcola la differenza tra il tuo schema e il database live e ti guida attraverso di essa. Ti viene sempre chiesto prima che venga applicato qualcosa. Ciò significa che una distribuzione di codice di routine non può mai attivare una migrazione di dati per errore.

Ogni modifica in sospeso ha uno stato che determina come migrate la tratta:

Stato Significato In migrate
safe Additivo e non bloccante — una nuova tabella, colonna o indice Applicato insieme in un unico passaggio, su conferma
warning Potenzialmente distruttivo o lento — eliminare qualcosa, o un indice su una tabella enorme Presentato uno alla volta, ognuno confermato separatamente
manual Non può essere fatto automaticamente — es. cambiare il tipo di una colonna Mostrato con guida; lo esegui a mano
undocumented Esiste nel database ma non nel tuo schema Mostrato per consapevolezza; non applicato

Le modifiche sicure sono rapide e reversibili nello spirito, quindi vengono applicate insieme. Ogni avviso è una conferma deliberata e individuale — non c’è un “applica tutto” per le modifiche distruttive.

Le modifiche manuali vengono fornite con un motivo e un’azione suggerita. migrate termina con un riepilogo: quante modifiche sono state applicate, saltate, in attesa di una correzione manuale o non nel tuo schema.

Vedi migrate per i flag (--dry-run, --safe, --yes, --local).

Rimuovere cose

Eliminare una tabella o una colonna da schema.js non la elimina — ciò renderebbe catastrofica una cancellazione accidentale. Per rimuovere qualcosa, contrassegnalo come deprecato:

// elimina una colonna
text('email').deprecated('sostituito da login')

// elimina un'intera tabella
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('inutilizzato');

Alla prossima migrate, l’oggetto deprecato viene mostrato come un’eliminazione con stato warning che confermi individualmente. Una volta sparito, rimuovi la dichiarazione.

Cambiare il tipo di una colonna

Le modifiche di tipo sono manuali — SQLite non può sempre farle sul posto, e costringere i valori esistenti è una scelta di giudizio. migrate mostrerà la modifica e il suo ragionamento; eseguila tu stesso con SQL grezzo (db.run(...)), tipicamente creando una nuova colonna o tabella, copiando i dati e scambiandoli.

L’SDK

A runtime un modulo ha una libreria: sdk. Raggruppa le tre cose di cui ha bisogno un backend di bot — un database, la Telegram Bot API e l’HTTP in uscita — senza nulla da installare e nessuna credenziale da configurare. Il database (db) è trattato in Il database ; questa sezione copre api, fetch e il globale console.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // l'intera superficie
// o da sottomoduli:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Import Cos’è
db Database — costruttore di query e DSL schema → Il database
api Telegram Bot API — api.sendMessage(...) → sotto
fetch HTTP in uscita → sotto
InputFile Un file (byte + nome) da caricare — funziona sia con api che con fetch → File
EndpointError Rifiuta una chiamata di una Mini App con un messaggio che può mostrare → Endpoint

Importa i tuoi moduli di progetto tramite percorso relativo con l’estensione .js (from '../schema.js', from '../lib/cart.js'). Vedi Il sistema dei moduli .

La Bot API

api ti fornisce l’intera Telegram Bot API . Chiama qualsiasi metodo come api.<metodo>(params). Ogni metodo Bot API attuale — e futuro — funziona senza necessità di aggiornamento SDK.

import { api } from 'sdk';

const me = await api.getMe();                            // → il risultato scartato
await api.sendMessage({ chat_id: id, text: 'Ciao!' });
await api.editMessageText({ chat_id, message_id, text: 'Aggiornato' });
await api.answerCallbackQuery({ callback_query_id, text: 'Fatto' });

L’involucro della risposta viene scartato. La Bot API normalmente avvolge i risultati in { ok: true, result: … }. api restituisce direttamente il result — getMe() risolve nell’oggetto utente, non in un involucro. I parametri usano i nomi snake_case della Bot API (chat_id, message_id, reply_markup, …).

I fallimenti lanciano BotApiError. Quando la Bot API restituisce { ok: false }, la chiamata lancia un BotApiError invece di restituire un valore falso, così non puoi ignorarlo accidentalmente. L’errore trasporta .code (il error_code della Bot API), .description (il messaggio leggibile dall’uomo), .method (quale metodo ha fallito) e .parameters (dati extra come retry_after su un 429, o migrate_to_chat_id). Catturalo per gestire un fallimento previsto e rilancia il resto:

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = il messaggio è già sparito; va bene così.
  } else {
    throw e;
  }
}

File

Lavora con i byte dei file, non solo con file_id — carica nuovi file e scarica quelli esistenti.

Caricamento — InputFile. Avvolgi i byte grezzi in un InputFile (un contenitore semplice: byte, un nome file e un type MIME facoltativo) e passalo ovunque un metodo Bot API si aspetti un file:

import { api, InputFile } from 'sdk';

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

// i file annidati nei parametri funzionano anche — es. 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' }) },
]});

I byte vengono trasmessi a Telegram come un vero caricamento multipart — il file non viene memorizzato interamente in una singola chiamata alla piattaforma — ma i limiti della Bot API di Telegram si applicano ancora: un bot può attualmente caricare fino a 50 MB per file (e 10 MB per una foto).

Download — tramite file_id. Due helper recuperano i byte di un file. La piattaforma risolve il file_id per te (esegue getFile e recupera il contenuto dietro le quinte), quindi non tocchi mai il token del bot o un URL di download:

const byte = await api.getFileContent(file_id);          // → Uint8Array (l'intero file)

const { file, body } = await api.getFileStream(file_id);  // trasmettilo invece
file.file_unique_id;                                      // le info getFile
for await (const chunk of body) { /* Uint8Array */ }      // o memorizzalo: await stream.bytes()

La Bot API di Telegram limita i download getFile a 20 MB, quindi quello è il tetto per entrambi gli helper.

HTTP

fetch è un client stile fetch per chiamare il mondo esterno — API di terze parti, webhook, qualsiasi cosa su 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 risposta rispecchia la piattaforma web: res.status, res.statusText, res.ok (true per 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) e i lettori di corpo await res.json() / await res.text().

Puoi anche leggere il corpo in modo incrementale come stream — for await (const chunk of res.body) { … } — che è come consumi eventi inviati dal server o output token-per-token dalle API AI.

Gli helper di corpo impostano il Content-Type corrispondente per te:

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('hi') });     // text/plain

I corpi delle richieste possono anche trasportare file e byte grezzi: passa un Uint8Array, un InputFile o un FormData (per multipart/form-data) come body, e i byte vengono trasmessi in uscita — quindi puoi caricare verso un’API di terze parti nello stesso modo in cui invii file a Telegram:

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

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

Altrimenti si comporta come il fetch standard che già conosci, con due vincoli:

  • La risposta viene letta come testo — await res.json() / await res.text(), o trasmessa pezzo per pezzo con res.body.
  • La risposta totale è limitata a 30 MB. Quel limite copre l’intera risposta — trasmettere con res.body ti permette di elaborare un corpo grande in modo incrementale, ma non alza il limite.

Registrazione

Il console globale standard è disponibile — nulla da importare, è lì come in qualsiasi JavaScript. Il suo output viene catturato e mostrato da npx tgcloud run, il che lo rende il tuo strumento di debug principale durante lo sviluppo.

console.log('elaborazione', { chatId: id });   // log / debug — semplice
console.info('avviato');                     // info — blu
console.warn('rate limited');                // warn — giallo
console.error(err);                          // error — rosso, con traccia dello stack

Ogni riga è contrassegnata con il [file:line] da cui proviene. console.error e console.trace aggiungono uno stack completo, mentre console.warn no. Quando fai npx tgcloud run di un modulo, queste righe vengono stampate con un prefisso colorato per livello, il tempo trascorso dall’inizio dell’esecuzione e l’origine — vedi run .

Interfaccia a riga di comando

tgcloud è il ponte tra la tua cartella di progetto e il cloud. Crea progetti, ti mostra cosa è cambiato, distribuisce, esegue moduli e applica migrazioni del database. Ha bisogno di Node.js 18 o superiore. Due modi per ottenerlo:

# Consigliato — crea un progetto con la CLI installata all'interno:
npm create @tgcloud/bot example_bot

# O installa la CLI globalmente e inizializza una cartella vuota:
npm install -g @tgcloud/cli
tgcloud init

Il pacchetto npm è @tgcloud/cli e il comando che installa è tgcloud. Ogni comando funziona da qualsiasi sottocartella del tuo progetto.

Comando Scopo
init Crea un nuovo progetto nella cartella corrente
add Crea un nuovo modulo (un gestore, un endpoint o un modulo lib)
login Collega il progetto a un bot (salva il token)
status Mostra cosa è cambiato localmente rispetto al cloud
diff Mostra le modifiche riga per riga
push Distribuisce i moduli modificati e la build della Mini App al cloud
migrate Applica le modifiche dello schema al database
run Esegue un modulo sulla piattaforma senza distribuire
fetch Controlla cosa è distribuito, senza cambiare i file locali
pull Allinea i file locali con il cloud
reset Scarta le modifiche locali; ripristina dallo stato cloud
webhook Ispeziona e risincronizza il webhook gestito dalla piattaforma
completion Stampa uno script di completamento shell (bash/zsh/fish)
upgrade Sposta un layout di progetto pre-0.2.0 in tgcloud/

Autenticazione

Un progetto è legato a un bot dal suo token, che ha la forma app<id>:<secret>. La parte app<id> è pubblica e può essere stampata; il segreto non appare mai nei log o negli errori.

Il token viene risolto in questo ordine:

  1. TGCLOUD_TOKEN variabile d’ambiente — per CI; mai scritto su disco.
  2. .tgcloud/credentials — scritto da npx tgcloud login .
  3. Nessuno dei due → un errore che ti indirizza a npx tgcloud login.

La CLI non richiede mai un token a metà comando. Il login è sempre il passaggio esplicito login. Se un token salvato diventa non valido, la CLI lo rimuove e ti chiede di fare di nuovo login.

init

npx tgcloud init

Crea un nuovo progetto nella directory corrente: la cartella tgcloud/ con schema.js, lib/, handlers/ e un gestore iniziale, più tgcloud.jsonc, AGENTS.md, docs/ e la cartella di stato .tgcloud/. Eseguilo anche nella radice di un progetto front‑end esistente — aggiunge la cartella tgcloud/ e i documenti, e non sovrascrive mai i tuoi file. Se riconosce il front‑end, punta anche tgcloud.jsonc alla cartella di build e aggiunge uno script deploy (costruisci, poi push) al tuo package.json — solo se non hai già uno script deploy. Vedi Front‑end della Mini App .

init rifiuta di creare un progetto all’interno di un altro progetto. Eseguirlo di nuovo nella radice di un progetto va bene e aggiunge solo ciò che manca.

add

npx tgcloud add <target>

Crea un singolo nuovo modulo, pronto per essere modificato — un gestore, un endpoint o un modulo lib/.

npx tgcloud add handlers/callback_query   # un nuovo gestore di aggiornamento
npx tgcloud add endpoints/getProfile      # un nuovo endpoint per la Mini App
npx tgcloud add lib/cart                  # un nuovo modulo condiviso

Nota che add non sovrascrive mai un file esistente.

Il <target> è il percorso del modulo (un .js finale è facoltativo). Per handlers/, il nome deve essere un tipo di aggiornamento Telegram. La piattaforma pubblicizza il set valido, quindi un nome non valido viene rifiutato subito. handlers/ e endpoints/ sono piatti, mentre lib/ può essere annidato (lib/payments/stripe).

Il nome del modulo è richiesto. Dare solo la directory è un errore — ma utile: per handlers/ elenca i tipi di aggiornamento che non hai ancora, così puoi copiarne uno.

$ npx tgcloud add handlers
Errore: Specifica un nome, es. "handlers/callback_query". Tipi di handlers/ disponibili: callback_query, inline_query, chat_member, …

Il completamento <Tab> offre lo stesso set — vedi completion .

Il file generato ha un export default attivo, quindi il gestore è attivo non appena distribuisci — non c’è nulla da decommentare. Distribuisci il nuovo modulo con push .

login

npx tgcloud login

Richiede il tuo token di accesso alla CLI — da @BotFather → il tuo bot → Serverless → CLI Access → Access token, un token separato dal token API del tuo bot — lo convalida con la piattaforma e lo salva in .tgcloud/credentials.

login è l’ unico comando che chiede un token e richiede un terminale interattivo. In CI, imposta TGCLOUD_TOKEN invece.

status

npx tgcloud status

Mostra, per file, cosa è cambiato tra la tua directory di lavoro e la copia distribuita: modificato, nuovo, eliminato, invariato. Funziona offline, confrontando con lo stato del cloud dal tuo ultimo push, pull o fetch. Avvisa anche riguardo a file .js randagi all’interno di tgcloud/ che non verranno distribuiti.

Per un progetto con una Mini App, status mostra anche cosa è cambiato nella sua cartella di build dall’ultima distribuzione.

diff

npx tgcloud diff

Come status, ma mostra le differenze effettive riga per riga per i moduli modificati. Anche offline.

push

npx tgcloud push [file...]

Distribuisce il tuo progetto nel cloud in un unico batch atomico.

Senza argomenti, distribuisce l’intero progetto e lo stato distribuito viene fatto riflettere esattamente la tua cartella — i moduli che hai eliminato localmente vengono rimossi nel cloud.

Con argomenti file o directory (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), distribuisce solo le modifiche in quei file. Un push mirato non elimina mai altri moduli. Un target può essere scritto come il percorso del file (tgcloud/handlers/message.js), il percorso all’interno di tgcloud/ (handlers/message.js) o il nome del modulo (handlers/message) — significano tutti lo stesso modulo.

La sua unica opzione è --force — per saltare il controllo di concorrenza e sovrascrivere ciò che c’è nel cloud. Usalo solo quando sei sicuro (vedi Mantenersi sincronizzati ).

Se tgcloud.jsonc nomina una cartella statica, un push completo la distribuisce nello stesso batch — vengono caricati solo i file che sono cambiati — e stampa l’indirizzo del sito. Costruisci prima, perché push non esegue mai la tua build (npm run deploy fa entrambi). npx tgcloud push dist/ distribuisce solo i file statici, e un push che nomina solo i moduli li lascia intatti.

Dopo una distribuzione, se schema.js è cambiato e il database non è sincronizzato, push stampa un riepilogo delle modifiche in sospeso e suggerisce npx tgcloud migrate. Non le applica mai da solo.

migrate

npx tgcloud migrate

Applica le modifiche del tuo schema al database. Calcola la differenza tra schema.js e il database live, poi ti guida attraverso di essa un passo alla volta con un contatore [N/M] in esecuzione:

  • Un breve riepilogo di tutte le modifiche in sospeso.
  • Modifiche sicure, applicate insieme in un unico passaggio alla tua conferma.
  • Avvisi (eliminazioni, operazioni lente), uno alla volta, ognuno confermato separatamente.
  • Modifiche manuali, mostrate con un motivo e un’azione suggerita, non applicate automaticamente.
  • Oggetti non documentati (nel database ma non nel tuo schema), mostrati per consapevolezza.

Termina con un riepilogo: applicate, saltate, in attesa di correzione manuale, non nello schema.

Vedi Migrazioni per il modello.

Opzioni: --dry-run (stampa tutto, non applica nulla), --safe (auto‑applica le modifiche sicure, salta gli avvisi), --yes (auto‑applica le modifiche sicure e ogni avviso, salta le manuali — usa con cautela), --local (confronta con il tuo schema.js locale invece di quello distribuito). Senza un flag, migrate richiede un terminale e genera un errore in un ambiente non interattivo invece di indovinare.

run

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

Esegue un gestore o un endpoint sulla piattaforma senza distribuire, utilizzando i tuoi file locali correnti. Questo è il ciclo interno rapido per testare la logica.

  • <modulo> — un nome semplice (cercato sotto handlers/ e endpoints/) o un percorso come handlers/message. Un nome semplice che esiste in entrambe le directory deve essere scritto con la sua directory.
  • [args] — il payload passato al gestore, scritto in JSON5 così puoi saltare le virgolette alle chiavi. È l’oggetto tipo-aggiornamento che il tuo gestore riceve (es. un Messaggio per handlers/message), o l’ input di un endpoint, e deve essere un oggetto — non un array o una stringa semplice.
  • --ctx <json5> — l’oggetto contesto del modulo (il suo secondo argomento), anche JSON5. Usalo per fornire ciò che il tuo codice legge da ctx — l’aggiornamento grezzo per un gestore, --ctx '{ update: { update_id: 1 } }', o i dati init per un endpoint, --ctx '{ initData: { user: { id: 1 } } }'. run non lo verifica, dato che stai testando la tua logica, non i controlli della piattaforma.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'

La piattaforma esegue il modulo contro lo spazio moduli assemblato dal tuo progetto locale (quindi viene usato anche il codice lib/ modificato localmente) e restituisce il valore di ritorno, qualsiasi cosa registrata con console.* e il tempo trascorso. Leggi gli argomenti grandi da un file con npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Controlla cosa è attualmente distribuito, senza toccare i tuoi file di lavoro. Dopo di esso, status e diff confrontano con l’ultimo stato del cloud. Utile per guardare un conflitto prima di decidere come risolverlo.

pull

npx tgcloud pull

Allinea il tuo progetto locale con il cloud aggiornando i tuoi file di lavoro allo stato distribuito.

pull non scrive mai la cartella di build della tua Mini App.

reset

npx tgcloud reset

Scarta le tue modifiche locali e ripristina i moduli dall’ultimo stato cloud conosciuto. Usalo per buttare via un esperimento. La cartella di build della Mini App non viene toccata.

webhook

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

Telegram consegna gli aggiornamenti al tuo bot tramite un webhook, che la piattaforma gestisce per te — non lo punti mai da nessuna parte a mano. npx tgcloud webhook mostra il suo stato attuale: l’URL, l’elenco allowed_updates, quanti aggiornamenti sono in sospeso, l’ultimo errore di consegna (se presente) e se è sincronizzato con i gestori che hai distribuito.

“Sincronizzato” significa che il webhook punta alla piattaforma e i suoi allowed_updates corrispondono ai gestori che hai distribuito — quindi Telegram consegna esattamente i tipi di aggiornamento che gestisci e nient’altro. Distribuire un nuovo gestore (o rimuoverne uno) può lasciare il webhook non sincronizzato finché non viene aggiornato; npx tgcloud status lo segnala anche.

npx tgcloud webhook sync lo corregge — punta di nuovo il webhook alla piattaforma e ricostruisce allowed_updates dai tuoi gestori distribuiti. Aggiungi --drop-pending per scartare gli aggiornamenti che Telegram aveva già accodato prima della sincronizzazione (altrimenti vengono consegnati una volta che il webhook è di nuovo sano).

completion

Nota: il completamento tab funziona solo quando un tgcloud semplice è nel tuo PATH — quindi installalo globalmente (npm install -g @tgcloud/cli) o metti altrimenti il binario nel tuo PATH. Non può agganciarsi a npx.

tgcloud completion <bash|zsh|fish>

Stampa uno script di completamento shell su stdout. Abilitalo una volta, poi <Tab> completa comandi, flag, directory dei moduli, i tipi di aggiornamento dei gestori che non hai ancora e i tuoi moduli locali eseguibili.

# bash — ha bisogno del pacchetto bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — assicurati che `autoload -U compinit && compinit` sia eseguito nel tuo ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

Riavvia la tua shell (o esegui di nuovo il file) in seguito. Eseguire tgcloud completion senza shell stampa di nuovo queste istruzioni.

upgrade

npx tgcloud upgrade [--dry-run]

Migrazione una tantum per progetti creati prima della CLI 0.2.0, che mantenevano schema.js, lib/ e handlers/ alla radice del progetto. Li sposta in tgcloud/. Il tuo collegamento al bot viene mantenuto e nulla deve essere ridistribuito. --dry-run stampa le mosse pianificate senza toccare nulla. Gli altri comandi non vengono eseguiti sul vecchio layout finché non esegui l’aggiornamento.

Mantenersi sincronizzati

Ogni progetto ha una revisione in costante aumento nel cloud, aumentata a ogni distribuzione. La CLI ricorda la revisione con cui si è sincronizzata l’ultima volta e la invia a ogni push. Se il cloud è andato avanti — perché un’altra macchina o compagno di squadra ha distribuito — il push viene rifiutato invece di sovrascrivere silenziosamente il loro lavoro, e la CLI offre tre modi per procedere:

npx tgcloud fetch           # vedi cosa è cambiato nel cloud, poi ricontrolla
npx tgcloud pull            # porta l'ultimo stato nei tuoi file di lavoro
npx tgcloud push --force    # sovrascrivi lo stato del cloud (pericoloso)

Questo controllo di concorrenza ottimistica è il motivo per cui puoi condividere un bot tra un team senza un processo di distribuzione a passo bloccato. I comandi escono non-zero in caso di fallimento — una distribuzione rifiutata, una migrazione fallita, un errore di autenticazione, un modulo che ha lanciato un errore durante run — quindi si compongono in modo pulito in script e pipeline CI.