O Telegram Serverless permite que você execute o código backend para seu bot e Mini App diretamente na infraestrutura do Telegram — sem servidores para provisionar, sem contêineres para manter ativos, sem preocupações com escalonamento. Você escreve módulos JavaScript puros, os implanta com um único comando, e o Telegram os executa em um sandbox V8 rápido e isolado que fica ao lado da Bot API e de um banco de dados integrado.

Se você já conectou um bot a um VPS, uma função em nuvem ou um painel de hospedagem apenas para responder a um /start, esta é a parte que você não precisa mais fazer.

Por que serverless

Um bot do Telegram é, em essência, um programa que reage a atualizações. Tradicionalmente, você precisava hospedar esse programa em algum lugar que estivesse sempre ativo, acessível e seguro — e mantê-lo assim. O Telegram Serverless remove essa camada inteiramente:

  • Sem infraestrutura. Não há máquina para alugar, corrigir ou monitorar. Seu código é executado sob demanda e escala com seu bot automaticamente.
  • Credenciais incluídas. A Telegram Bot API, um banco de dados com suporte a SQLite e HTTP de saída estão disponíveis para cada módulo “pronto para uso” — nada para instalar, nenhuma credencial para configurar.
  • Execução rápida e isolada. Cada invocação é executada em um isolado V8 leve, próximo aos próprios sistemas do Telegram, para que as chamadas à Bot API e ao seu banco de dados sejam rápidas e confiáveis.
  • Um fluxo de trabalho de desenvolvedor real. Um projeto vive em uma pasta em sua máquina sob controle de versão. Você edita arquivos, vê exatamente o que mudou, implanta atomicamente e avança o esquema do seu banco de dados com migrações revisadas — da maneira que você já trabalha com todo o resto.

O modelo mental: Como o projeto funciona

Você trabalha em três lugares, e eles se mapeiam limpos uns aos outros:

Onde O que vive lá
Sua pasta de projeto Módulos JavaScript — esquema, código compartilhado, manipuladores de atualização, endpoints de Mini App — e, se você tiver um, o frontend do Mini App
A nuvem A cópia implantada desses módulos, o banco de dados do seu bot e os arquivos do Mini App
O CLI tgcloud A ponte — ele mostra diferenças e as sincroniza

Você nunca faz SSH em nada. Você edita arquivos localmente, executa npx tgcloud push, e a plataforma cuida do resto. O tráfego do seu bot é tratado pelos módulos implantados; seu banco de dados persiste entre as invocações.

Um projeto tem apenas quatro tipos de código, todos dentro de uma pasta tgcloud/:

tgcloud/
├─ endpoints/  # funções que seu Mini App chama
├─ handlers/   # funções que reagem a tipos de atualização do Telegram
├─ lib/        # código compartilhado que você importa de qualquer lugar
└─ schema.js   # suas tabelas de banco de dados

Quando uma atualização chega — uma mensagem, um clique de botão, uma consulta inline — o Telegram a roteia para o manipulador correspondente (handlers/message.js, handlers/callback_query.js, …) e chama seu export padrão. Essa função interage com a Bot API e o banco de dados por meio do SDK e retorna. Esse é o loop completo. Uma atualização sem um manipulador correspondente é simplesmente ignorada, então você adiciona apenas os manipuladores de que precisa.

Um Mini App é opcional. Se o seu bot tiver um, o frontend dele viverá no mesmo projeto, ao lado de tgcloud/. O Serverless hospeda a saída da compilação dele e executa os endpoints/ que ele chama. Veja Frontend de Mini App .

Demonstração rápida

Aqui está um bot de demonstração completo e funcional. Ele responde a cada mensagem e lembra quantas viu de cada 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;

  // Insere o contador, ou incrementa se este chat já tiver um — e obtém a
  // linha resultante na mesma declaração 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: `Olá! Eu vi ${row.seen} mensagem(s) sua.`,
  });
}

Implante-o:

npx tgcloud push       # envia os módulos
npx tgcloud migrate    # cria a tabela `counters`

Isso é um bot ativo com estado persistente e sem servidor. Tudo nele — api, db, o DSL table() — é descrito nas seções abaixo.

Serverless é um backend geral para bots do Telegram e Mini Apps, não um modelo para um tipo de aplicativo. É ideal para:

  • Bots de IA conversacional que precisam armazenar estado por usuário em um banco de dados.
  • Backends de Mini App que armazenam dados do usuário e servem conteúdo dinâmico.
  • Jogos e Ferramentas — incluindo placares, quizzes e mais.
  • Automações e Integrações que chamam APIs HTTP de terceiros e enviam resultados para chats.

Primeiros passos

Este guia leva você de uma pasta vazia a um bot ativo que responde a mensagens e armazena dados. Ele assume que você tem o Node.js 18 ou mais recente instalado e um bot registrado com o @BotFather . Ao final, você terá usado todos os comandos necessários no dia a dia: push, migrate, run e status.

Primeiro, ative o Serverless. No @BotFather , abra seu bot → Serverless e ative-o. Isso ativa o recurso para este bot e desbloqueia seu token de acesso ao CLI, manipuladores, biblioteca e banco de dados.

1. Crie um projeto

A maneira mais rápida de começar é com o criador de projetos, que estrutura um projeto e instala o CLI nele:

npm create @tgcloud/bot example_bot
cd example_bot

O argumento é a pasta de destino: passe . para estruturar na pasta atual ou qualquer caminho. Ele também funciona em uma pasta existente e nunca sobrescreve arquivos que você já tem.

Isso lhe dá um projeto pronto para editar:

example_bot/
├─ tgcloud/             # tudo que a plataforma executa
│  ├─ handlers/
│  │  └─ message.js     # um manipulador de mensagem inicial (ecoando o texto de volta)
│  ├─ lib/              # seus módulos compartilhados vão aqui (vazio para começar)
│  └─ schema.js         # suas tabelas de banco de dados
├─ docs/
│  └─ tgcloud-sdk.md    # referência do SDK (para você e suas ferramentas de IA)
├─ AGENTS.md            # orientação para assistentes de codificação de IA
├─ package.json
└─ tgcloud.jsonc        # configuração do projeto

Os arquivos estruturados são autossuficientes — cada um contém exemplos comentados do que você pode fazer a seguir.

O CLI é instalado no projeto como uma dependência de desenvolvimento local, então você o executa com npx tgcloud <command> (npx encontra a cópia no node_modules do seu projeto), ou através dos atalhos npm run que o scaffold adiciona ao package.json (npm run deploy, npm run status). Por padrão, não há tgcloud global no seu PATH.

Você também pode instalá-lo globalmente — npm install -g @tgcloud/cli — se preferir digitar um tgcloud puro de qualquer lugar. Isso é útil para executar tgcloud init em qualquer pasta vazia, e é o que o preenchimento automático do shell precisa. De qualquer forma, você obtém o mesmo projeto.

Construindo um Mini App? Comece pelo frontend dele. Crie-o com sua ferramenta usual, então execute o criador de projetos nessa pasta. Veja Frontend de Mini App .

2. Conecte seu bot

Cada projeto está vinculado a um bot. Conecte-os com login, que solicita seu token de acesso ao CLI ( @BotFather → seu bot → Serverless → CLI Access → Access token — um token separado do seu token de API do bot ) e o armazena localmente:

npx tgcloud login

O token tem o formato app<id>:<secret>. O CLI o mantém em .tgcloud/, que é ignorado pelo git, e nunca imprime a parte secreta. O login é a única vez que ele é solicitado — veja Autenticação para como os tokens são resolvidos em CI.

3. Dê uma olhada

Dois comandos informam o estado das coisas a qualquer momento, ambos totalmente offline:

npx tgcloud status     # o que mudou localmente vs. a cópia implantada
npx tgcloud diff       # as mudanças linha a linha

Logo após o init, tudo é novo e nada ainda foi implantado. status mostra os arquivos iniciais esperando para serem enviados.

4. Implante

Envie seus módulos para a nuvem:

npx tgcloud push

push envia todos os módulos alterados em um lote atômico e atualiza seu registro local do que a nuvem agora detém. Seu bot está ativo: abra-o no Telegram e envie uma mensagem — o manipulador inicial o ecoa de volta.

A implantação nunca mexe no seu banco de dados. Enviar código e alterar o esquema do seu banco de dados são etapas deliberadamente separadas, para que uma implantação de código nunca possa surpreendê-lo com uma migração de dados. É para isso que serve a próxima etapa.

5. Adicione uma tabela ao banco de dados

Vamos fazer o bot lembrar de algo. Abra tgcloud/schema.js e declare uma tabela:

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

Implante o esquema, então aplique-o ao banco de dados:

npx tgcloud push       # envia o novo esquema
npx tgcloud migrate    # cria a tabela `messages`

push relata que o esquema está dessincronizado e mostra a mudança pendente, mas não aplica nada. migrate percorre a mudança e, mediante sua confirmação, cria a tabela. Este modelo de duas etapas — e o que acontece com mudanças mais arriscadas como exclusões — é abordado em Migrações .

6. Armazene e leia dados

Agora use a tabela do seu manipulador. Edite 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 esta mensagem.
  await db.insert(messages)
    .values({ chatId: message.chat.id, text: message.text })
    .run();

  // Conta quantas salvamos para este chat.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Salvo. São ${count} mensagem(s) deste chat até agora.`,
  });
}

Implante o manipulador atualizado com npx tgcloud push, então envie algumas mensagens para o seu bot e veja a contagem aumentar. O banco de dados persiste entre as invocações — essa é a memória do seu bot.

7. Teste sem implantar

Você não precisa implantar para testar uma mudança. npx tgcloud run executa um manipulador na plataforma usando seus arquivos locais, sem publicá-los:

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

O argumento é a carga útil que seu manipulador recebe — para handlers/message, uma Mensagem — escrita em JSON5 (para que você possa pular as aspas nas chaves). O comando imprime qualquer coisa que o manipulador registrou com console.*, o valor de retorno e quanto tempo levou. Este é o loop mais rápido para iterar na lógica — sem implantação, sem esperar por uma mensagem real.

8. Mantenha a sincronização

À medida que você trabalha, alguns comandos mantêm seu projeto local e a nuvem alinhados: npx tgcloud status mostra o que mudou, npx tgcloud push implanta, npx tgcloud pull traz seu projeto local para alinhar com a nuvem, npx tgcloud fetch verifica o que está implantado sem alterar seus arquivos, e npx tgcloud reset descarta as mudanças locais.

Se duas pessoas (ou duas máquinas) implantarem no mesmo bot, a plataforma detectará o conflito e push parará para que você pull primeiro — você nunca poderá sobrescrever silenciosamente o trabalho de outra pessoa. Veja Mantendo a sincronização .

Construindo com IA

Prefere construir com um assistente de IA — ou o único codificador na sua equipe é uma IA? Você ainda pode enviar um bot. Demos um primeiro passo para fazer um agente de IA se sentir em casa em um projeto: cada novo é estruturado com um AGENTS.md e uma referência docs/tgcloud-sdk.md que as ferramentas de codificação de agentes leem automaticamente.

Junto com um runtime pequeno e autocontido — um SDK, nenhum pacote npm para gerenciar — isso dá ao assistente um bom começo nas convenções que a geração de código genérica tende a perder aqui: importações relativas com .js, sem chaves estrangeiras, cada chamada db é assíncrona, um manipulador por tipo de atualização e o fluxo de push/migrate em duas etapas.

Tente:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # ou Claude Code, Cursor, … — qualquer agente que leia AGENTS.md

Então apenas peça, em linguagem simples:

Escreva um bot que lembre a lista de tarefas de cada pessoa — adicione um item quando eles enviarem texto e mostre a lista inteira quando eles enviarem /list.

O assistente edita schema.js e seus manipuladores para você; você revisa, testa uma mudança instantaneamente com npx tgcloud run, então vai para o ar com npx tgcloud push e npx tgcloud migrate. AGENTS.md faz parte do seu projeto — edite-o conforme o bot cresce para que a orientação permaneça precisa.

Em movimento com o BotFather

Reduzido a apenas seu telefone? Todo o projeto vive no @BotFather também — abra seu bot → Serverless e você obtém tudo que o CLI gerencia, em uma tela sensível ao toque:

  • Handlers — crie, edite e teste manipuladores de atualização; o BotFather mantém o webhook sincronizado com os manipuladores que você tem (o mesmo Em sincronia / Fora de sincronia que o CLI relata).
  • Endpoints — as funções que seu Mini App chama.
  • Biblioteca — seus módulos lib/ compartilhados.
  • Banco de Dados — edite schema.js na mesma sintaxe semelhante a Drizzle, revise mudanças pendentes e aplique-as; Salvar implanta.
  • Arquivos de Mini App — o frontend que seus usuários abrem no Telegram.
  • Acesso ao CLI — pegue o token de acesso ao CLI aqui quando estiver de volta a um teclado.

É um e o mesmo projeto na nuvem, então você pode iniciar um manipulador no seu telefone e npx tgcloud pull para seu laptop mais tarde — nada está vinculado a um único cliente. Executar um manipulador até mostra sua saída do Console diretamente no chat, assim como npx tgcloud run .

Projetos e módulos

Um projeto serverless é uma pasta comum sob controle de versão. Tudo o que a plataforma executa fica em uma pasta, tgcloud/; o restante do projeto é seu — um frontend de mini aplicativo, configuração, docs. Não há etapa de compilação para o bot, nenhum node_modules em tempo de execução e nenhum ponto de entrada do servidor.

Anatomia de um projeto

example_bot/
├─ tgcloud/             # a parte implantada — nada mais é
│  ├─ handlers/         # manipuladores de atualização — plano, um único nível
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # funções que seu Mini App chama — plano, um único nível
│  │  └─ getProfile.js
│  ├─ lib/              # módulos compartilhados; subdiretórios permitidos
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # esquema do banco de dados — um arquivo
└─ .tgcloud/            # estado do CLI, ignorado pelo git

Apenas arquivos .js dentro de tgcloud/ — schema.js, lib/, handlers/, endpoints/ — são implantados. Todo o resto — Markdown, arquivos de configuração, a pasta .tgcloud/, qualquer coisa fora de tgcloud/ — permanece em sua máquina. Os nomes dos módulos são relativos a tgcloud/. O arquivo tgcloud/handlers/message.js é o módulo handlers/message, e esse é o nome que npx tgcloud run usa.

Atualizando de um projeto mais antigo? Projetos criados antes do CLI 0.2.0 mantinham schema.js, lib/ e handlers/ na raiz do projeto. Execute npx tgcloud upgrade uma vez para movê-los para tgcloud/.

schema.js — seu banco de dados. Ele declara tabelas como exports nomeados usando o DSL de esquema e vive em tgcloud/ como um único arquivo. Ele é implantado como qualquer outro módulo, mas implantá-lo nunca altera o banco de dados — as alterações de esquema são aplicadas separadamente com npx tgcloud migrate. Veja O banco de dados .

lib/ — código compartilhado, qualquer coisa que você queira reutilizar entre manipuladores: utilitários puros, camadas de acesso ao banco de dados, formatação, integrações com serviços externos. lib/ é o único diretório que pode conter subdiretórios (lib/internal/util.js, lib/payments/stripe.js), para que você possa organizar um código maior como quiser. Módulos em lib/ nunca são invocados diretamente pela plataforma; eles existem para serem importados por manipuladores e por eles mesmos.

handlers/ — os pontos de entrada do seu bot. Cada arquivo corresponde a um tipo de atualização do Telegram, e a plataforma roteia cada atualização recebida para o manipulador correspondente:

Arquivo Lida com
handlers/message.js Novas mensagens recebidas
handlers/inline_query.js Novas consultas inline recebidas
handlers/callback_query.js Novas consultas de callback recebidas
… qualquer outro tipo de atualização da Bot API

handlers/ é plano — sem subdiretórios. O export default de um manipulador é a função que a plataforma chama (veja Handlers ).

Um tipo de atualização é tratado apenas se seu arquivo de manipulador existir e não estiver vazio. Se não houver handlers/<type>.js — ou o arquivo estiver vazio — atualizações desse tipo são ignoradas, e a plataforma não executa nada para elas. Portanto, mantenha apenas os manipuladores de que você realmente precisa: cada um que você adiciona é outro tipo de atualização para o qual seu bot acorda para processar, e omitir os outros significa que o Telegram não ativa seu código para atualizações que você apenas descartaria de qualquer maneira.

Para gerar um novo manipulador, execute npx tgcloud add handlers/<type> .

endpoints/ — o lado do servidor do seu Mini App. Cada arquivo é uma função que o Mini App chama pelo nome, então endpoints/getProfile.js responde a POST /api/getProfile. A plataforma verifica se a chamada vem de um usuário do Telegram dentro do seu Mini App antes que seu código seja executado, e entrega esse usuário para você. endpoints/ é plano, como handlers/. Veja Endpoints .

.tgcloud/ — estado local da máquina gerenciado inteiramente pelo CLI, incluindo suas credenciais salvas. Ele é ignorado pelo git, e você nunca deve ler ou escrever nele manualmente — use os comandos do CLI em vez disso.

O sistema de módulos

Em tempo de execução, um módulo pode ver exatamente duas coisas: o SDK da plataforma e os outros módulos do seu projeto. Não há pacotes npm, nenhum sistema de arquivos e nenhuma rede, exceto através do fetch do SDK.

Importe seus próprios módulos da maneira que faria em qualquer projeto JavaScript — por caminho relativo, com a extensão .js — e o SDK pelo seu nome:

// tgcloud/handlers/message.js
import { users } from '../schema.js';              //  o módulo de esquema
import { addItem } from '../lib/cart.js';          //  um módulo lib
import { format } from '../lib/internal/fmt.js';  //  módulo lib aninhado
import { db, api, fetch } from 'sdk';              //  o SDK da plataforma
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        //  um vizinho
import { users } from '../schema.js';

Uma importação relativa deve terminar em .js e não pode sair de tgcloud/:

import { users } from '../schema';       //  adicione o .js
import config from '../../config.js';    //  fora de tgcloud/
import { z } from 'zod';                 //  sem pacotes npm

Cada módulo também tem um nome — seu caminho dentro de tgcloud/ sem a extensão, como schema, lib/cart ou handlers/message. Esse nome é o que npx tgcloud run e a saída do CLI usam, e um módulo também pode ser importado por ele (from 'lib/cart', sem extensão). Prefira importações relativas em seu código. Elas são o que seu editor entende — ir para a definição, autocompletar, renomear.

O que está disponível em tempo de execução são exatamente duas coisas: sdk e seus submódulos (sdk/db, sdk/api, sdk/fetch) — toda a superfície da plataforma, veja O SDK — e seus próprios módulos em tgcloud/. Essa é a lista completa. Se seu código importar qualquer outra coisa, ele não será resolvido. Essa restrição é o que mantém os módulos rápidos para carregar e seguros para executar.

Handlers

Um manipulador é um módulo em handlers/ cujo export padrão é invocado pela plataforma quando uma atualização correspondente chega.

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Você disse: ${message.text ?? '(sem texto)'}`,
  });
}

Um manipulador recebe a carga útil da atualização como seu argumento — a plataforma desempacota o Update do Telegram para você. handlers/message.js recebe a Message (ou seja, update.message); handlers/callback_query.js recebe o CallbackQuery ; e assim por diante. O segundo argumento do manipulador é um objeto de contexto por invocação, ctx. Ele carrega o ctx.update bruto — use-o quando precisar de algo fora da carga útil, como update_id.

Um manipulador pode ser async (geralmente é) e pode retornar um valor. Ele acessa a Bot API, o banco de dados e o HTTP de saída através do SDK.

Você não precisa de uma atualização real para testar um manipulador. npx tgcloud run o executa na plataforma com a carga útil que você fornece e seu código local atual:

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

O argumento é a carga útil — o mesmo objeto que seu manipulador recebe — em JSON5. Para fornecer o ctx do manipulador (seu segundo argumento), adicione --ctx, por exemplo, --ctx '{ update: { update_id: 1 } }'. Isso é executado contra seus arquivos locais, para que você possa experimentar mudanças antes de implantá-las. Veja run .

Endpoints

Um endpoint é um módulo em endpoints/ que seu Mini App chama — o lugar para qualquer coisa que o frontend não possa fazer sozinho, como ler e escrever no banco de dados, chamar a Bot API ou interagir com serviços externos com seus segredos.

// 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('Perfil não encontrado', { userId: user.id });
  return profile;
}

O primeiro argumento, input, é o objeto JSON que o Mini App enviou. O segundo, ctx, carrega ctx.initData — os dados de inicialização do Mini App (user, chat, start_param, auth_date, …), já verificados pela plataforma. Ele tem a mesma forma que Telegram.WebApp.initDataUnsafe no cliente, mas aqui você pode confiar nele. Uma chamada sem dados de inicialização válidos é rejeitada com 401 antes que seu código seja executado, então ctx.initData.user é realmente a pessoa que abriu seu Mini App. Endpoints só podem ser chamados dessa maneira — do Mini App do seu bot, por um usuário do Telegram — então não há API pública para proteger e nenhum login para construir.

O que quer que o endpoint retorne é enviado de volta como JSON. Para recusar uma chamada, throw new EndpointError(description, parameters?). O Mini App recebe a descrição e o objeto parameters conforme estão. Se você precisar ramificar com base no tipo de erro no frontend, coloque um código em parameters — { code: 'OUT_OF_STOCK' }. Qualquer outra exceção é relatada como um erro interno genérico, e a mensagem real vai apenas para seus logs, então os internos nunca vazam para o cliente.

Chamando um endpoint do Mini App

O Mini App chama um endpoint pelo nome com Telegram.WebApp.Serverless.call do SDK oficial de Mini Apps , telegram-web-app.js — o script que seu Mini App já inclui. Ele envia os dados de inicialização para você e chama de volta com um erro ou o resultado do endpoint:

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — a descrição; err.parameters — o que seu EndpointError carregou
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

A entrada é opcional e deve ser um objeto simples. Sem ela, a chamada é Serverless.call('getProfile', callback).

err.type é 'ENDPOINT_ERROR' quando seu endpoint lançou um EndpointError. Qualquer outro valor significa que a plataforma recusou a chamada ou o endpoint falhou inesperadamente, por exemplo, 'UNAUTHORIZED' quando os dados de inicialização estão ausentes ou inválidos.

Nomes de endpoint são como identificadores JavaScript — letras, dígitos e _, não começando com um dígito (getProfile, place_order). Gere um com npx tgcloud add endpoints/<name> , e teste-o sem o Mini App com run , passando os dados de inicialização você mesmo:

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

O que é implantado

Quando você executa npx tgcloud push, o CLI coleta todos os arquivos .js sob schema.js, lib/, handlers/ e endpoints/, e envia esse conjunto exato como o espaço de módulos do seu projeto — juntamente com a pasta de compilação do Mini App, se tgcloud.jsonc nomear uma (veja abaixo). Qualquer coisa presente na nuvem, mas ausente do seu projeto, é removida, então o estado implantado sempre espelha sua pasta — exclusões incluídas. Nada fora de tgcloud/ é implantado como código — o restante da raiz do projeto é seu. Dentro de tgcloud/, um arquivo .js que não está em nenhum desses locais (tgcloud/util.js, tgcloud/helpers/x.js) é sinalizado, para que não seja implantado silenciosamente. Markdown, arquivos de ponto e .tgcloud/ nunca são implantados.

Frontend de Mini App

A raiz do projeto é sua, e é o lugar natural para o frontend do seu Mini App — construído com React, Vue, Svelte ou HTML puro, por Vite ou qualquer outro bundler. Serverless hospeda sua saída de compilação ao lado do seu bot. Um push implanta o código e os arquivos estáticos juntos, e o aplicativo é servido em https://app<app_id>.tgcloud.ai/ (push imprime o endereço exato — use-o como o URL do seu Mini App em @BotFather ).

Denunciando abuso. Mini Apps no tgcloud.ai são publicados por desenvolvedores independentes. Para denunciar conteúdo ilegal ou abuso em um subdomínio app<app_id>.tgcloud.ai, envie um e-mail para [email protected] .

Para começar, crie o frontend primeiro, com qualquer ferramenta que desejar, então adicione Serverless por cima dele:

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

O criador de projetos nunca sobrescreve seus arquivos. Ele adiciona a pasta tgcloud/ ao lado do frontend, reconhece a pasta de compilação e adiciona um script npm run deploy que compila o aplicativo e o implanta junto com o bot.

O que é hospedado é definido em tgcloud.jsonc, o arquivo de configuração do projeto na raiz do projeto:

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

Apenas "source" é obrigatório. É a pasta de compilação, relativa à raiz do projeto. As outras chaves de "static" configuram como ela é servida (abaixo). init cria tgcloud.jsonc para você. Quando ele reconhece o frontend, ele também preenche "source":

Frontend init define "source" para
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular nada — configure uma exportação estática primeiro, então defina "source" para a pasta dela você mesmo

Com qualquer outra configuração, defina "source" para sua pasta de compilação você mesmo.

A linha $schema dá ao seu editor autocompletar e validação para cada chave. O arquivo é JSON com comentários e vírgulas finais permitidas. Se outra ferramenta sua precisar lê-lo como JSON puro, nomeie-o como tgcloud.json em vez disso — então ele deve ser JSON estrito.

Compile antes de enviar. push envia a pasta exatamente como está no disco — exceto arquivos e pastas cujos nomes começam com um ponto, que são ignorados — e nunca executa sua compilação, então um simples npx tgcloud push após editar o frontend enviaria a compilação anterior. Use npm run deploy (ele executa seu script build, depois tgcloud push), ou compile você mesmo primeiro.

As opções de serviço:

  • spa — sirva index.html para caminhos que não existem e não têm extensão de arquivo, para que um roteador do lado do cliente funcione ao recarregar. Caminhos com extensão ainda recebem 404.
  • immutable — padrões para arquivos com hash de conteúdo (Vite’s /assets/*) que os navegadores podem armazenar em cache por um ano. Use apenas para arquivos com hash de conteúdo. Qualquer outro arquivo correspondido aqui permaneceria desatualizado nos navegadores por um ano, e não há como recuperá-lo.
  • headers — cabeçalhos de resposta para os caminhos que uma regra corresponde (um caminho exato, ou um prefixo terminando em /*). Um valor null remove um cabeçalho.
  • redirects — regras de redirecionamento, onde a primeira correspondência vence. Um arquivo existente sempre vence uma regra. status é 301, a menos que você o defina.
  • "static": "dist" — um atalho para { "source": "dist" } quando você não precisa de mais nada.
  • "static": false — remove o site implantado no próximo push. Deletar a chave em vez disso apenas interrompe este projeto de gerenciar arquivos estáticos, e o que estiver implantado permanece.

O site é servido a partir da raiz de seu próprio host, então a base padrão do seu bundler (/) está correta. O Telegram Web abre Mini Apps em um iframe, então não adicione X-Frame-Options ou uma política restritiva frame-ancestors.

O mesmo host responde aos seus endpoints sob /api/ — é para onde Telegram.WebApp.Serverless.call('getProfile', …) vai. Não coloque arquivos seus sob /api/ na pasta de compilação.

O banco de dados

Cada bot ganha seu próprio banco de dados — um armazenamento com suporte a SQLite que persiste entre as invocações e está disponível para cada módulo através de db. Você descreve suas tabelas em schema.js com um DSL pequeno e tipado; você as lê e escreve com um construtor de consulta fluente; e você as evolui com migrações revisadas.

Conhece o Drizzle ORM ? Então você já sabe como interagir com o banco de dados aqui. O DSL de esquema e o construtor de consulta seguem o Drizzle — os construtores de coluna, select().from().where(), os operadores, onConflictDoUpdate, .returning(), e a tag sql se comportam como você esperaria, então ler e escrever dados é a API familiar que você já usa. Você apenas o importa de sdk/db, e algumas especificidades da plataforma (notavelmente sem chaves estrangeiras ) são apontadas onde elas surgem.

Declarando tabelas

Tabelas são exports nomeados em schema.js. Chamar table() constrói uma descrição no tempo de carregamento (não mexe no banco de dados); a plataforma descobre as tabelas exportadas quando você implanta schema.js e migra o banco de dados para corresponder.

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(name, columns, extras?):

  • name é o nome da tabela SQL;
  • columns é um mapa de propriedade JS → definição de coluna;
  • extras é um callback opcional (t) => ({ … }) onde t expõe as colunas (t.created é uma referência a essa coluna) — declare índices e restrições em nível de tabela aqui.

Tipos de coluna

Fábrica Tipo SQLite Notas
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB lê/escreve Uint8Array
boolean() INTEGER armazenado como 0/1, lido como true/false
json() TEXT JSON.stringify / JSON.parse automático

O argumento do nome da coluna é opcional — omita-o e a chave JS é usada. A opção mode controla como os valores são convertidos entre SQLite e JavaScript.

mode Armazenado como Valor JS
boolean INTEGER 0/1 boolean
json TEXT (JSON) qualquer objeto/array
timestamp INTEGER (segundos unix) Date
timestamp_ms INTEGER (milissegundos unix) Date
bytes BLOB Uint8Array

boolean() e json() são atalhos para integer(name, { mode: 'boolean' }) e text(name, { mode: 'json' }).

Um blob() lê e escreve um Uint8Array — o runtime não tem o Buffer do Node (e um Buffer é uma subclasse de Uint8Array, então este é o tipo base portável). O mode acima governa apenas como um valor é codificado, independentemente do tipo de armazenamento da coluna: então blob('col', { mode: 'json' }) é a contraparte BLOB de json() — a mesma codificação JSON, mantida em uma coluna BLOB em vez de TEXT.

RESUMO: blob() usa Uint8Array. mode controla a codificação, não o armazenamento, então blob(..., { mode: 'json' }) armazena JSON como um BLOB, enquanto json() o armazena como TEXT.

Modificadores de coluna

Modificadores de coluna encadeiam em uma coluna.

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('substituído por login')   // marca a coluna para remoção

.default() em uma coluna json() codifica o valor para você; um sql…``default é passado verbatim..deprecated()` é terminal — veja Migrações .

Índices e restrições

Índices e restrições são declarados no callback extras, onde as colunas estão em escopo.

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})`) ,          // índice de expressão
  active: index('idx_active').on(t.userId).where(sql`done = 0`) ,  // índice parcial
}));

Modificadores em nível de tabela encadeiam após table(...): .strict(), .withoutRowid(), .deprecated('reason').

Sem chaves estrangeiras

O runtime é executado com PRAGMA foreign_keys desativado. Uma chave estrangeira declarada seria silenciosamente inerte — sem cascateamentos, sem proteção contra órfãos — o que é pior do que não ter nenhuma, então o DSL o torna impossível. Ou seja, .references() e foreignKey() em nível de tabela lançam um erro quando declarados, e um esquema que os usa não será implantado.

Você deve modelar relacionamentos com colunas simples (userId: integer('user_id')) e impor integridade no código da sua aplicação: insira pais antes dos filhos, exclua filhos antes dos pais, trate erros adequadamente e limpe órfãos com um LEFT JOIN … WHERE parent.id IS NULL quando precisar.

A ausência de chaves estrangeiras é uma restrição deliberada, não uma falha. Considere-a desde o início ao planejar seu bot.

Consultando

db é um construtor de consulta fluente. Cada consulta é assíncrona — os métodos terminais (.all(), .get(), .values(), .run()) retornam Promessas, então sempre use 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();                          // todas as linhas
await db.select().from(todos).where(eq(todos.id, 1)).get();   // primeira linha ou null
await db.select().from(todos).values();                       // linhas como arrays de valores

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

// projeção personalizada: { 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();

// contagem de linhas — um atalho, não um terminal de construtor:
await db.$count(todos);                        // todas as linhas
await db.$count(todos, eq(todos.done, false)); // com um filtro
  • Encadeáveis: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Terminais: .all(), .get(), .values().
  • Conte linhas com db.$count(table, where?) (ou count() em uma projeção).

Inserir, atualizar, excluir:

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

Note que uma inserção em lote é uma única declaração, então ela é limitada pelo limite de variáveis do SQLite (linhas × colunas); se você exceder isso, a inserção falhará — divida-a você mesmo.

Operadores são importados 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) com múltiplos argumentos é o mesmo que and(a, b). O segundo argumento de uma comparação é um valor por padrão, mas pode ser outra coluna ou um sql… — por exemplo, eq(a.x, b.y). Agregados (count/sum/avg/min/max) são fragmentos SQL para uma projeção .select({ … }).

SQL Bruto — quando o construtor não é suficiente, desça para SQL bruto. O modo é escolhido pelo método: db.run para escritas, db.all para muitas linhas, db.get para uma linha, db.values para linhas como arrays posicionais.

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, 'comprar leite']]

Uma escrita resolve para um resultado de execução — rowsAffected, lastInsertRowid, e rows, que carregam uma cláusula RETURNING se o SQL a teve. Use db.values quando uma consulta projeta duas colunas com o mesmo nome (um join como SELECT u.id, o.id): db.all chaveia linhas por nome de coluna, então o segundo id sobrescreveria o primeiro.

A tag sql`…` transforma ${value} em um parâmetro vinculado e ${table.column} em um identificador, e insere fragmentos sql aninhados. Use sql.raw('…') para um literal sem parâmetros.

Consultas brutas não são vinculadas a uma tabela, então suas linhas voltam sem conversão de modo — um booleano é 0/1, uma coluna JSON é uma string, um timestamp é um número. Apenas o construtor vinculado à tabela converte valores.

Migrações

Seu banco de dados muda conforme seu bot cresce. A plataforma mantém isso seguro separando a implantação de código da mudança de dados, e classificando cada alteração de esquema por quão arriscada ela é.

A implantação nunca mexe no banco de dados. Quando você npx tgcloud push um schema.js alterado, a plataforma registra o novo esquema e informa como o banco de dados mudaria — mas não aplica nada:

npx tgcloud push       # implanta schema.js; relata mudanças pendentes no DB
npx tgcloud migrate    # revisa e aplica

npx tgcloud migrate calcula a diferença entre schema.js e o banco de dados ativo e o percorre passo a passo com um contador [N/M] em execução:

  • Um breve resumo de todas as mudanças pendentes.
  • Mudanças seguras, aplicadas juntas em uma única etapa, mediante sua confirmação.
  • Avisos (exclusões, operações lentas), um por vez, cada um confirmado separadamente.
  • Mudanças manuais, exibidas com uma razão e sugestão de ação, não aplicadas automaticamente.
  • Objetos não documentados (no banco de dados, mas não no seu esquema), exibidos para conscientização.

Ele termina com um resumo: aplicados, ignorados, aguardando correção manual, não no esquema.

Veja migrate para as flags (--dry-run, --safe, --yes, --local).

Removendo coisas

Excluir uma tabela ou coluna de schema.js não a remove — isso tornaria uma exclusão acidental catastrófica. Para remover algo, marque-o como obsoleto:

// excluir uma coluna
text('email').deprecated('substituído por login')

// excluir uma tabela inteira
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('não usado');

Na próxima migrate, o objeto obsoleto aparece como uma exclusão de status de aviso que você confirma individualmente. Uma vez removido, remova a declaração.

Mudando o tipo de uma coluna

Mudanças de tipo são manuais — o SQLite nem sempre consegue fazê-las no local, e a coerção de valores existentes é uma decisão pessoal. migrate mostrará a mudança e seu raciocínio; execute-a você mesmo com SQL bruto (db.run(...)), tipicamente criando uma nova coluna ou tabela, copiando dados e trocando.

O SDK

Em tempo de execução, um módulo tem uma biblioteca: sdk. Ele agrupa as três coisas que um backend de bot precisa — um banco de dados, a Telegram Bot API e HTTP de saída — sem necessidade de instalação e sem credenciais para configurar. O banco de dados (db) é abordado em O banco de dados ; esta seção aborda api, fetch e o global console.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // toda a superfície
// ou de submódulos:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Importação O que é
db Banco de dados — construtor de consulta e DSL de esquema → O banco de dados
api Telegram Bot API — api.sendMessage(...) → abaixo
fetch HTTP de saída → abaixo
InputFile Um arquivo (bytes + nome) para upload — funciona com api e fetch → Arquivos
EndpointError Recusa uma chamada do Mini App com uma mensagem que ele pode exibir → Endpoints

Importe seus próprios módulos de projeto por caminho relativo com a extensão .js (from '../schema.js', from '../lib/cart.js'). Veja O sistema de módulos .

A Bot API

api fornece toda a Telegram Bot API . Chame qualquer método como api.<method>(params). Cada método atual — e futuro — da Bot API funciona sem atualização do SDK necessária.

import { api } from 'sdk';

const me = await api.getMe();                            // → o resultado desempacotado
await api.sendMessage({ chat_id: id, text: 'Olá!' });
await api.editMessageText({ chat_id, message_id, text: 'Atualizado' });
await api.answerCallbackQuery({ callback_query_id, text: 'Feito' });

O envelope de resposta é desempacotado. A Bot API normalmente envolve resultados em { ok: true, result: … }. api retorna o result diretamente — getMe() resolve para o objeto do usuário, não para um wrapper. Os parâmetros usam os próprios nomes snake_case da Bot API (chat_id, message_id, reply_markup, …).

Falhas lançam BotApiError. Quando a Bot API retorna { ok: false }, a chamada lança um BotApiError em vez de retornar um valor falso, para que você não possa ignorá-lo acidentalmente. O erro carrega .code (o error_code da Bot API), .description (a mensagem legível por humanos), .method (qual método falhou) e .parameters (dados extras como retry_after em um 429, ou migrate_to_chat_id). Capture-o para tratar uma falha esperada e relance o restante:

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = a mensagem já foi excluída; tudo bem aqui.
  } else {
    throw e;
  }
}

Arquivos

Trabalhe com bytes de arquivo, não apenas com file_ids — envie novos arquivos e baixe os existentes.

Enviar — InputFile. Envolva bytes brutos em um InputFile (um contêiner simples: bytes, um nome de arquivo e um type MIME opcional) e passe-o onde quer que um método da Bot API espere um arquivo:

import { api, InputFile } from 'sdk';

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

// arquivos aninhados dentro de parâmetros também funcionam — por exemplo, um álbum:
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' }) },
]});

Os bytes são transmitidos para o Telegram como um upload multipart real — o arquivo não é tamponado inteiro em uma única chamada para a plataforma — mas os limites da Bot API do Telegram ainda se aplicam: um bot pode atualmente enviar até 50 MB por arquivo (e 10 MB para uma foto).

Baixar — por file_id. Dois atalhos puxam os bytes de um arquivo de volta. A plataforma resolve o file_id para você (ela executa getFile e busca o conteúdo nos bastidores), então você nunca toca no token do bot ou em um URL de download:

const bytes = await api.getFileContent(file_id);          // → Uint8Array (o arquivo inteiro)

const { file, body } = await api.getFileStream(file_id);  // transmita em vez disso
file.file_unique_id;                                      // as informações getFile
for await (const chunk of body) { /* Uint8Array */ }      // ou bufferize: await stream.bytes()

Os downloads getFile da Bot API do Telegram são limitados a 20 MB, então esse é o limite para ambos os atalhos.

HTTP

fetch é um cliente semelhante a fetch para chamar o mundo exterior — APIs de terceiros, webhooks, qualquer coisa via 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();

A resposta espelha a plataforma web: res.status, res.statusText, res.ok (verdadeiro para 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) e leitores de corpo await res.json() / await res.text().

Você também pode ler o corpo incrementalmente como um stream — for await (const chunk of res.body) { … } — que é como você consome eventos enviados pelo servidor ou saída token a token de APIs de IA.

Os atalhos de corpo definem o Content-Type correspondente para você:

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

Corpos de requisição também podem carregar arquivos e bytes brutos: passe um Uint8Array, um InputFile ou um FormData (para multipart/form-data) como body, e os bytes são transmitidos — então você pode enviar para uma API de terceiros da mesma forma que envia arquivos para o 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', 'do meu bot');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });

Caso contrário, ele se comporta como o fetch padrão que você já conhece, com duas restrições:

  • A resposta é lida como texto — await res.json() / await res.text(), ou transmitida pedaço a pedaço com res.body.
  • A resposta total é limitada a 30 MB. Esse limite abrange toda a resposta — transmitir com res.body permite que você processe um corpo grande incrementalmente, mas não aumenta o limite.

Logging

O global padrão console está disponível — nada para importar, está lá como em qualquer JavaScript. Sua saída é capturada e exibida por npx tgcloud run, o que o torna sua principal ferramenta de depuração durante o desenvolvimento.

console.log('processando', { chatId: id });   // log / debug — simples
console.info('iniciado');                     // info  — azul
console.warn('taxa limitada');                // warn  — amarelo
console.error(err);                          // error — vermelho, com um stack trace

Cada linha é marcada com o [file:line] de onde veio. console.error e console.trace adicionam um stack trace completo, enquanto console.warn não. Quando você executa npx tgcloud run um módulo, essas linhas são impressas com um prefixo colorido por nível, o tempo desde o início da execução e a origem — veja run .

Interface de linha de comando

tgcloud é a ponte entre sua pasta de projeto e a nuvem. Ele estrutura projetos, mostra o que mudou, implanta, executa módulos e aplica migrações de banco de dados. Requer Node.js 18 ou mais recente. Duas maneiras de obtê-lo:

# Recomendado — crie um projeto com o CLI instalado nele:
npm create @tgcloud/bot example_bot

# Ou instale o CLI globalmente e inicialize uma pasta vazia:
npm install -g @tgcloud/cli
tgcloud init

O pacote npm é @tgcloud/cli, e o comando que ele instala é tgcloud. Cada comando funciona a partir de qualquer subpasta do seu projeto.

Comando Propósito
init Estrutura um novo projeto na pasta atual
add Estrutura um novo módulo (um manipulador, um endpoint ou um módulo lib)
login Vincula o projeto a um bot (salva o token)
status Mostra o que mudou localmente vs. a nuvem
diff Mostra as mudanças linha a linha
push Implanta módulos alterados e a compilação do Mini App na nuvem
migrate Aplica mudanças de esquema ao banco de dados
run Executa um módulo na plataforma sem implantar
fetch Verifica o que está implantado, sem alterar arquivos locais
pull Traz os arquivos locais para alinhar com a nuvem
reset Descarta alterações locais; restaura do estado da nuvem
webhook Inspeciona e resincroniza o webhook gerenciado pela plataforma
completion Imprime um script de preenchimento para o shell (bash/zsh/fish)
upgrade Move um layout de projeto pré-0.2.0 para tgcloud/

Autenticação

Um projeto é vinculado a um bot por seu token, que tem o formato app<id>:<secret>. A parte app<id> é pública e pode ser impressa; o segredo nunca aparece em logs ou erros.

O token é resolvido na seguinte ordem:

  1. Variável de ambiente TGCLOUD_TOKEN — para CI; nunca gravado em disco.
  2. .tgcloud/credentials — gravado por npx tgcloud login .
  3. Nenhum dos dois → um erro apontando para npx tgcloud login.

O CLI nunca solicita um token durante o comando. Fazer login é sempre a etapa explícita de login. Se um token salvo se tornar inválido, o CLI o remove e pede para você fazer login novamente.

init

npx tgcloud init

Estrutura um novo projeto no diretório atual: a pasta tgcloud/ com schema.js, lib/, handlers/ e um manipulador inicial, mais tgcloud.jsonc, AGENTS.md, docs/, e a pasta de estado .tgcloud/. Execute-o na raiz de um projeto frontend existente também — ele adiciona a pasta tgcloud/ e os docs, e nunca sobrescreve seus arquivos. Se ele reconhecer o frontend, ele também aponta tgcloud.jsonc para a pasta de compilação e adiciona um script deploy (compilar, depois enviar) ao seu package.json — apenas se você ainda não tiver um script deploy. Veja Frontend de Mini App .

init se recusa a criar um projeto dentro de outro projeto. Executá-lo novamente na raiz do próprio projeto é bom e apenas adiciona o que estiver faltando.

add

npx tgcloud add <target>

Estrutura um único novo módulo, conectado e pronto para editar — um manipulador, um endpoint ou um módulo lib/.

npx tgcloud add handlers/callback_query   # um novo manipulador de atualização
npx tgcloud add endpoints/getProfile      # um novo endpoint para o Mini App
npx tgcloud add lib/cart                  # um novo módulo compartilhado

Note que add nunca sobrescreve um arquivo existente.

O <target> é o caminho do módulo (um .js final é opcional). Para handlers/, o nome deve ser um tipo de atualização do Telegram. A plataforma anuncia o conjunto válido, então um nome inválido é rejeitado antecipadamente. handlers/ e endpoints/ são planos, enquanto lib/ pode ser aninhado (lib/payments/stripe).

O nome do módulo é obrigatório. Dar apenas o diretório é um erro — mas um útil: para handlers/ ele lista os tipos de atualização que você ainda não tem, então você pode copiar um.

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

O preenchimento <Tab> oferece o mesmo conjunto — veja completion .

O arquivo gerado tem um export default ativo, então o manipulador está ativo assim que você implanta — não há nada para descomentar. Implante o novo módulo com push .

login

npx tgcloud login

Solicita seu token de acesso ao CLI — de @BotFather → seu bot → Serverless → CLI Access → Access token, um token separado do seu token de API do bot — o valida contra a plataforma e o salva em .tgcloud/credentials.

login é o único comando que solicita um token e requer um terminal interativo. Em CI, defina TGCLOUD_TOKEN em vez disso.

status

npx tgcloud status

Mostra, por arquivo, o que mudou entre seu diretório de trabalho e a cópia implantada: modificado, novo, excluído, inalterado. Funciona offline, comparando com o estado da nuvem a partir do seu último push, pull ou fetch. Também alerta sobre arquivos .js perdidos dentro de tgcloud/ que não serão implantados.

Para um projeto com um Mini App, status também mostra o que mudou na pasta de compilação desde a última implantação.

diff

npx tgcloud diff

Semelhante a status, mas mostra as diferenças reais linha a linha para módulos alterados. Também offline.

push

npx tgcloud push [files...]

Implanta seu projeto na nuvem em um único lote atômico.

Sem argumentos, ele implanta o projeto inteiro, e o estado implantado passa a espelhar exatamente sua pasta — módulos que você excluiu localmente são removidos na nuvem.

Com argumentos de arquivo ou diretório (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/) , ele implanta apenas as mudanças nesses arquivos. Um push direcionado nunca exclui outros módulos. Um alvo pode ser escrito como o caminho do arquivo (tgcloud/handlers/message.js) , o caminho dentro de tgcloud/ (handlers/message.js) ou o nome do módulo (handlers/message) — todos significam o mesmo módulo.

Sua única opção é --force — para pular a verificação de concorrência e sobrescrever o que quer que esteja na nuvem. Use-o apenas quando tiver certeza (veja Mantendo a sincronização ).

Se tgcloud.jsonc nomear uma pasta estática, um push completo a implanta no mesmo lote — apenas os arquivos que mudaram são enviados — e imprime o endereço do site. Compile primeiro, pois push nunca executa sua compilação (npm run deploy faz ambos). npx tgcloud push dist/ implanta apenas os arquivos estáticos, e um push que nomeia apenas módulos os deixa intocados.

Após uma implantação, se schema.js mudou e o banco de dados está dessincronizado, push imprime um resumo das mudanças pendentes e sugere npx tgcloud migrate. Ele nunca as aplica.

migrate

npx tgcloud migrate

Aplica suas mudanças de esquema ao banco de dados. Ele calcula a diferença entre schema.js e o banco de dados ativo, então o percorre passo a passo com um contador em execução [N/M]:

  • Um resumo das mudanças pendentes.
  • Mudanças seguras, aplicadas juntas em uma única etapa, mediante sua confirmação.
  • Avisos (exclusões, operações lentas), um por vez, cada um confirmado separadamente.
  • Mudanças manuais, exibidas com uma razão e sugestão de ação, não aplicadas automaticamente.
  • Objetos não documentados (no banco de dados, mas não no seu esquema), exibidos para conscientização.

Ele termina com um resumo: aplicados, ignorados, aguardando correção manual, não no esquema.

Veja Migrações para o modelo.

Opções: --dry-run (imprimir tudo, não aplicar nada), --safe (aplicar automaticamente mudanças seguras, pular avisos), --yes (aplicar automaticamente mudanças seguras e todos os avisos, pular manuais — use com cuidado), --local (comparar com seu schema.js local em vez do implantado). Sem um flag, migrate requer um terminal e falha em um ambiente não interativo em vez de adivinhar.

run

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

Executa um manipulador ou um endpoint na plataforma sem implantar, usando seus arquivos locais atuais. Este é o loop interno rápido para testar a lógica.

  • <module> — um nome simples (pesquisado em handlers/ e endpoints/) ou um caminho como handlers/message. Um nome simples que existe em ambos os diretórios deve ser escrito com seu diretório.
  • [args] — a carga útil passada para o manipulador, escrita em JSON5 para que você possa pular as aspas das chaves. É o objeto do tipo de atualização que seu manipulador recebe (por exemplo, uma Mensagem para handlers/message), ou a input de um endpoint, e deve ser um objeto — não um array ou uma string simples.
  • --ctx <json5> — o objeto de contexto do módulo (seu segundo argumento), também JSON5. Use-o para fornecer o que seu código lê de ctx — a atualização bruta para um manipulador, --ctx '{ update: { update_id: 1 } }', ou os dados de inicialização para um endpoint, --ctx '{ initData: { user: { id: 1 } } }'. run não o verifica, já que você está testando sua lógica, não as verificações da plataforma.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "oi" }'

A plataforma executa o módulo contra o espaço de módulos montado a partir do seu projeto local (então o código lib/ alterado localmente também é usado) e retorna o valor de retorno, qualquer coisa registrada com console.* e o tempo decorrido. Leia argumentos grandes de um arquivo com npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Verifica o que está implantado atualmente, sem mexer nos seus arquivos de trabalho. Após executá-lo, status e diff comparam com o último estado da nuvem. Útil para olhar um conflito antes de decidir como resolvê-lo.

pull

npx tgcloud pull

Traz seu projeto local para alinhar com a nuvem atualizando seus arquivos de trabalho para o estado implantado.

pull nunca escreve na pasta de compilação do seu Mini App.

reset

npx tgcloud reset

Descarta suas alterações locais e restaura os módulos do último estado conhecido da nuvem. Use-o para jogar fora um experimento. A pasta de compilação do Mini App não é tocada.

webhook

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

O Telegram entrega atualizações ao seu bot através de um webhook, que a plataforma gerencia para você — você nunca o aponta para lugar nenhum manualmente. npx tgcloud webhook mostra seu estado atual: o URL, a lista allowed_updates, quantas atualizações estão pendentes, o último erro de entrega (se houver) e se está em sincronia com seus manipuladores implantados.

“Em sincronia” significa que o webhook aponta para a plataforma e sua lista allowed_updates corresponde aos manipuladores que você implantou — para que o Telegram entregue exatamente os tipos de atualização que você lida, e nada mais. Implantar um novo manipulador (ou remover um) pode deixar o webhook dessincronizado até que ele seja atualizado; npx tgcloud status também sinaliza isso.

npx tgcloud webhook sync corrige isso — ele reposiciona o webhook para a plataforma e reconstrói allowed_updates a partir dos seus manipuladores implantados. Adicione --drop-pending para descartar atualizações que o Telegram já enfileirou antes da sincronização (caso contrário, elas serão entregues assim que o webhook estiver saudável novamente).

completion

Observação: o preenchimento automático por tabulação funciona apenas quando um tgcloud simples está no seu PATH — então instale-o globalmente (npm install -g @tgcloud/cli) ou, de outra forma, coloque o binário no seu PATH. Ele não pode se conectar ao npx.

tgcloud completion <bash|zsh|fish>

Imprime um script de preenchimento para o shell na saída padrão. Habilite-o uma vez, então <Tab> completará comandos, flags, diretórios de módulos, os tipos de atualização de manipulador que você ainda não tem e seus módulos executáveis locais.

# bash — precisa do pacote bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — certifique-se de que `autoload -U compinit && compinit` seja executado no seu ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

Reinicie seu shell (ou reabasteça o arquivo) depois. Executar tgcloud completion sem um shell imprime estas instruções novamente.

upgrade

npx tgcloud upgrade [--dry-run]

Migração única para projetos criados antes do CLI 0.2.0, que mantinham schema.js, lib/ e handlers/ na raiz do projeto. Ele os move para dentro de tgcloud/. Seu link com o bot é mantido, e nada precisa ser reimplantado. --dry-run imprime as movimentações planejadas sem mexer em nada. Outros comandos não são executados no layout antigo até que você atualize.

Mantendo a sincronização

Cada projeto tem uma revisão monotonicamente crescente na nuvem, incrementada a cada implantação. O CLI lembra a revisão com a qual ele sincronizou pela última vez e a envia em cada push. Se a nuvem avançou — porque outra máquina ou colega de equipe implantou — o push é rejeitado em vez de sobrescrever silenciosamente o trabalho deles, e o CLI oferece três caminhos a seguir:

npx tgcloud fetch           # veja o que mudou na nuvem, então verifique novamente
npx tgcloud pull            # traga o último para seus arquivos de trabalho
npx tgcloud push --force    # sobrescreva o estado da nuvem (perigoso)

Essa verificação de concorrência otimista é o motivo pelo qual você pode compartilhar um bot em uma equipe sem um processo de implantação de sincronização obrigatória. Os comandos saem com um valor diferente de zero em caso de falha — um deploy rejeitado, uma migração falha, um erro de autenticação, um módulo que lançou uma exceção durante run — então eles se compõem limpos em scripts e pipelines de CI.