Telegram Serverless te permite ejecutar código backend para tu bot y Mini App directamente en la infraestructura de Telegram — sin servidores que aprovisionar, sin contenedores que mantener activos, sin escalado en el que pensar. Escribes módulos simples de JavaScript, los despliegas con un solo comando, y Telegram los ejecuta en un sandbox V8 rápido y aislado que se encuentra justo al lado de la Bot API y una base de datos integrada.

Si alguna vez has conectado un bot a un VPS, una función en la nube o un panel de hosting solo para responder a un /start, esta es la parte que ya no tienes que hacer.

Por qué serverless

Un bot de Telegram es, en esencia, un programa que reacciona a las actualizaciones. Tradicionalmente, tenías que alojar ese programa en algún lugar que estuviera siempre encendido, accesible y seguro — y luego mantenerlo así. Telegram Serverless elimina esa capa por completo:

  • Sin infraestructura. No hay máquina que alquilar, parchear o monitorizar. Tu código se ejecuta bajo demanda y se escala automáticamente con tu bot.
  • Credenciales incluidas. La Telegram Bot API, una base de datos basada en SQLite y el HTTP saliente están disponibles para cada módulo de fábrica — nada que instalar, ninguna credencial que configurar.
  • Ejecución rápida y aislada. Cada invocación se ejecuta en un aislado V8 ligero, cerca de los propios sistemas de Telegram, por lo que las llamadas a la Bot API y a tu base de datos son rápidas y fiables.
  • Un flujo de trabajo de desarrollador real. Un proyecto vive en una carpeta en tu máquina bajo control de versiones. Editas archivos, ves exactamente qué cambió, despliegas atómicamente y haces evolucionar el esquema de tu base de datos con migraciones revisadas — la forma en que ya trabajas con todo lo demás.

El modelo mental: Cómo funciona el proyecto

Trabajas en tres lugares, y se mapean limpiamente uno sobre otro:

Dónde Qué vive allí
Tu carpeta de proyecto Módulos JavaScript — esquema, código compartido, manejadores de actualizaciones, endpoints de Mini App — y, si tienes uno, el frontend de Mini App
La nube La copia desplegada de esos módulos, la base de datos de tu bot y los archivos de Mini App
La CLI tgcloud El puente — te muestra diferencias y las sincroniza

Nunca haces SSH a nada. Editas archivos localmente, ejecutas npx tgcloud push, y la plataforma se encarga del resto. El tráfico de tu bot es manejado por los módulos desplegados; tu base de datos persiste entre invocaciones.

Un proyecto tiene solo cuatro tipos de código, todos dentro de una carpeta tgcloud/:

tgcloud/
├─ endpoints/  # funciones que tu Mini App llama
├─ handlers/   # funciones que reaccionan a tipos de actualización de Telegram
├─ lib/        # código compartido que importas desde cualquier lugar
└─ schema.js   # tus tablas de base de datos

Cuando llega una actualización — un mensaje, la pulsación de un botón, una consulta inline — Telegram la dirige al manejador correspondiente (handlers/message.js, handlers/callback_query.js, …) y llama a su exportación por defecto. Esa función habla con la Bot API y la base de datos a través del SDK, y retorna. Ese es el bucle completo. Una actualización sin un manejador coincidente se ignora simplemente, así que solo agregas los manejadores que necesitas.

Una Mini App es opcional. Si tu bot tiene una, su frontend vive en el mismo proyecto, junto a tgcloud/. Serverless aloja su salida de compilación y ejecuta los endpoints/ que llama. Ver Frontend de Mini App .

Demostración rápida

Aquí tienes un bot de demostración completo y funcional. Responde a cada mensaje y recuerda cuántos ha visto 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;

  // Inserta el contador, o lo incrementa si este chat ya tiene uno — y obtiene la
  // fila resultante en la misma sentencia a través de .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: `¡Hola! He visto ${row.seen} mensaje(s) tuyo(s).`,
  });
}

Despliega esto:

npx tgcloud push       # sube los módulos
npx tgcloud migrate    # crea la tabla `counters`

Eso es un bot en vivo con estado persistente y sin servidor. Todo en él — api, db, el DSL table() — se describe en las secciones siguientes.

Serverless es un backend general para bots y Mini Apps de Telegram, no una plantilla para un tipo de aplicación. Es ideal para:

  • Bots de IA conversacional que necesitan almacenar estado por usuario en una base de datos.
  • Backends de Mini App que almacenan datos de usuario y sirven contenido dinámico.
  • Juegos y herramientas — incluyendo tablas de clasificación, quizzes y más.
  • Automatizaciones e integraciones que llaman a APIs HTTP de terceros y envían resultados a chats.

Introducción

Este tutorial te lleva desde una carpeta vacía hasta un bot en vivo que responde a mensajes y almacena datos. Asume que tienes Node.js 18 o superior instalado y un bot registrado con @BotFather . Al final, habrás utilizado todos los comandos que necesitas día a día: push, migrate, run y status.

Antes que nada, activa Serverless. En @BotFather , abre tu bot → Serverless y actívalo. Esto activa la función para este bot y desbloquea su token de acceso a la CLI, manejadores, librería y base de datos.

1. Crea un proyecto

La forma más rápida de empezar es el creador de proyectos, que genera un proyecto y lo instala la CLI en él:

npm create @tgcloud/bot example_bot
cd example_bot

El argumento es la carpeta de destino: pasa . para generar en la carpeta actual, o cualquier ruta. También funciona en una carpeta existente y nunca sobrescribe archivos que ya tengas.

Esto te da un proyecto listo para editar:

example_bot/
├─ tgcloud/             # todo lo que la plataforma ejecuta
│  ├─ handlers/
│  │  └─ message.js     # un manejador de mensajes inicial (repite el texto)
│  ├─ lib/              # tus módulos compartidos van aquí (vacío para empezar)
│  └─ schema.js         # tus tablas de base de datos
├─ docs/
│  └─ tgcloud-sdk.md    # referencia del SDK (para ti y tus herramientas de IA)
├─ AGENTS.md            # orientación para asistentes de codificación de IA
├─ package.json
└─ tgcloud.jsonc        # configuración del proyecto

Los archivos generados se autoinscriben — cada uno contiene ejemplos comentados de lo que puedes hacer a continuación.

La CLI se instala en el proyecto como una dependencia de desarrollo local, por lo que la ejecutas con npx tgcloud <comando> (npx encuentra la copia en node_modules de tu proyecto) , o a través de los atajos npm run que el generador agrega a package.json (npm run deploy, npm run status). Por defecto, no hay un tgcloud global en tu PATH.

También puedes instalarlo globalmente — npm install -g @tgcloud/cli — si prefieres escribir tgcloud desnudo desde cualquier lugar. Eso es útil para ejecutar tgcloud init en cualquier carpeta vacía, y es lo que necesita el autocompletado de la shell. De cualquier manera obtienes el mismo proyecto.

¿Construyendo una Mini App? Empieza desde su frontend en su lugar. Créala con tu herramienta habitual, luego ejecuta el creador de proyectos en esa carpeta. Ver Frontend de Mini App .

2. Vincula tu bot

Cada proyecto está vinculado a un bot. Conéctalos con login, que solicita tu token de acceso a la CLI ( @BotFather → tu bot → Serverless → CLI Access → Access token — un token separado de tu token de API de bot ) y lo guarda localmente:

npx tgcloud login

El token tiene la forma app<id>:<secret>. La CLI lo guarda en .tgcloud/, que se ignora en git, y nunca imprime la parte secreta. Login es la única vez que se te pide — ver Autenticación para cómo se resuelven los tokens en CI.

3. Mira alrededor

Dos comandos te dicen dónde están las cosas en cualquier momento, ambos completamente sin conexión:

npx tgcloud status     # qué ha cambiado localmente vs. la copia desplegada
npx tgcloud diff       # los cambios línea por línea

Justo después de init todo es nuevo y nada está desplegado todavía. status muestra los archivos iniciales esperando a ser subidos.

4. Despliega

Envía tus módulos a la nube:

npx tgcloud push

push sube cada módulo modificado en un lote atómico y actualiza tu registro local de lo que tiene la nube. Tu bot está en vivo: ábrelo en Telegram y envíale un mensaje — el manejador inicial lo repite.

Desplegar nunca toca tu base de datos. Subir código y cambiar el esquema de tu base de datos son pasos deliberadamente separados, por lo que un despliegue de código nunca puede sorprenderte con una migración de datos. Eso es para lo que sirve el siguiente paso.

5. Añade una tabla de base de datos

Hagamos que el bot recuerde algo. Abre tgcloud/schema.js y declara una tabla:

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

Despliega el esquema, luego aplícalo a la base de datos:

npx tgcloud push       # sube el nuevo esquema
npx tgcloud migrate    # crea la tabla `messages`

push informa que el esquema no está sincronizado y te muestra el cambio pendiente, pero no aplica nada. migrate te guía a través del cambio y, tras tu confirmación, crea la tabla. Este modelo de dos pasos — y lo que sucede con cambios más arriesgados como drops — se cubre en Migraciones .

6. Almacena y lee datos

Ahora usa la tabla desde tu manejador. Edita tgcloud/handlers/message.js:

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

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

  // Cuenta cuántos hemos almacenado para este chat.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Guardado. Eso son ${count} mensaje(s) de este chat hasta ahora.`,
  });
}

Despliega el manejador actualizado con npx tgcloud push, luego envía algunos mensajes a tu bot y observa cómo aumenta el contador. La base de datos persiste entre invocaciones — esa es la memoria de tu bot.

7. Prueba sin desplegar

No tienes que desplegar para probar un cambio. npx tgcloud run ejecuta un manejador en la plataforma usando tus archivos locales, sin publicarlos:

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

El argumento es el payload que tu manejador recibe — para handlers/message, un Message — escrito en JSON5 (así que puedes omitir las comillas de las claves). El comando imprime cualquier cosa que el manejador haya registrado con console.*, el valor de retorno y cuánto tardó. Este es el bucle más ajustado para iterar sobre la lógica — sin despliegue, sin esperar un mensaje real.

8. Mantente sincronizado

Mientras trabajas, unos pocos comandos mantienen tu proyecto local y la nube alineados: npx tgcloud status muestra qué cambió, npx tgcloud push despliega, npx tgcloud pull trae tu proyecto local a línea con la nube, npx tgcloud fetch verifica qué está desplegado sin tocar tus archivos, y npx tgcloud reset descarta los cambios locales.

Si dos personas (o dos máquinas) despliegan al mismo bot, la plataforma detecta el conflicto y push se detiene para que primero hagas pull — nunca podrás sobrescribir el trabajo de otro silenciosamente. Ver Mantenerse sincronizado .

Construyendo con IA

¿Prefieres construir con un asistente de IA — o es la IA el único programador de tu equipo? Aún puedes lanzar un bot. Hemos dado un primer paso para que un agente de IA se sienta como en casa en un proyecto: cada uno nuevo se genera con un AGENTS.md y una referencia docs/tgcloud-sdk.md que las herramientas de codificación agentizadas leen automáticamente.

Junto con un runtime pequeño y autocontenido — un SDK, sin paquetes npm que gestionar — eso le da al asistente un buen comienzo en las convenciones que elcodegen genérico tiende a pasar por alto aquí: importaciones relativas con .js, sin claves foráneas, cada llamada a db es asíncrona, un manejador por tipo de actualización, y el flujo de dos pasos push/migrate.

Pruébalo:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # o Claude Code, Cursor, … — cualquier agente que lea AGENTS.md

Luego, simplemente pregunta, en lenguaje natural:

Escribe un bot que recuerde la lista de tareas de cada persona — agrega un elemento cuando envíen texto, y muestra la lista completa cuando envíen /list.

El asistente edita schema.js y tus manejadores por ti; tú revisas, pruebas un cambio instantáneamente con npx tgcloud run, luego lo pones en vivo con npx tgcloud push y npx tgcloud migrate. AGENTS.md es parte de tu proyecto — edítalo a medida que el bot crece para que la guía se mantenga precisa.

En movimiento con BotFather

¿En tu teléfono? Todo el proyecto vive también en @BotFather — abre tu bot → Serverless y obtienes todo lo que la CLI gestiona, en una pantalla táctil:

  • Manejadores — crea, edita y ejecuta manejadores de actualización de prueba; BotFather mantiene el webhook sincronizado con los manejadores que tienes (el mismo estado Sincronizado / Desincronizado que reporta la CLI).
  • Endpoints — las funciones que llama tu Mini App.
  • Librería — tus módulos lib/ compartidos.
  • Base de datos — edita schema.js con la misma sintaxis similar a Drizzle, revisa los cambios pendientes y aplícalos; Guardar despliega.
  • Archivos de Mini App — el frontend que tus usuarios abren en Telegram.
  • Acceso CLI — obtén el token de acceso a la CLI aquí cuando vuelvas a un teclado.

Es el mismo proyecto en la nube, así que puedes empezar un manejador en tu teléfono y descargarlo con npx tgcloud pull en tu portátil más tarde — nada está atado a un solo cliente. Ejecutar un manejador incluso muestra su salida de Consola directamente en el chat, igual que npx tgcloud run .

Proyectos y módulos

Un proyecto serverless es una carpeta ordinaria bajo control de versiones. Todo lo que la plataforma ejecuta se encuentra en una sola carpeta, tgcloud/; el resto del proyecto es tuyo — un frontend de mini-app, configuración, documentación. No hay paso de compilación para el bot, no hay node_modules en tiempo de ejecución, y no hay punto de entrada de servidor.

Anatomía de un proyecto

example_bot/
├─ tgcloud/             # la parte desplegada — nada más lo está
│  ├─ handlers/         # manejadores de actualización — planos, un solo nivel
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # funciones que tu Mini App llama — planos, un solo nivel
│  │  └─ getProfile.js
│  ├─ lib/              # módulos compartidos; se permiten subdirectorios
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # esquema de base de datos — un archivo
└─ .tgcloud/            # estado de la CLI, ignorado por git

Solo los archivos .js dentro de tgcloud/ — schema.js, lib/, handlers/, endpoints/ — son desplegados. Todo lo demás — Markdown, archivos de configuración, la carpeta .tgcloud/, cualquier cosa fuera de tgcloud/ — permanece en tu máquina. Los nombres de los módulos son relativos a tgcloud/. El archivo tgcloud/handlers/message.js es el módulo handlers/message, y ese es el nombre que toma npx tgcloud run.

¿Actualizando desde un proyecto anterior? Los proyectos creados antes de la CLI 0.2.0 mantenían schema.js, lib/ y handlers/ en la raíz del proyecto. Ejecuta npx tgcloud upgrade una vez para moverlos dentro de tgcloud/.

schema.js — tu base de datos. Declara tablas como exportaciones nombradas usando el DSL de esquema y vive en tgcloud/ como un solo archivo. Se despliega como cualquier otro módulo, pero desplegarlo nunca cambia la base de datos — los cambios de esquema se aplican por separado con npx tgcloud migrate. Ver La base de datos .

lib/ — código compartido, cualquier cosa que quieras reutilizar entre manejadores: helpers puros, capas de acceso a la base de datos, formato, integraciones con servicios externos. lib/ es el único directorio que puede contener subdirectorios (lib/internal/util.js, lib/payments/stripe.js), así que puedes organizar una base de código más grande como prefieras. Los módulos en lib/ nunca son invocados directamente por la plataforma; existen para ser importados por manejadores y entre sí.

handlers/ — los puntos de entrada de tu bot. Cada archivo corresponde a un tipo de actualización de Telegram, y la plataforma dirige cada actualización entrante al manejador coincidente:

Archivo Maneja
handlers/message.js Nuevos mensajes entrantes
handlers/inline_query.js Nuevas consultas inline entrantes
handlers/callback_query.js Nuevas consultas de devolución de llamada entrantes
… cualquier otro tipo de actualización de Bot API

handlers/ es plano — sin subdirectorios. La export default de un manejador es la función que la plataforma llama (ver Manejadores ).

Un tipo de actualización se maneja solo si su archivo manejador existe y no está vacío. Si no hay handlers/<type>.js — o el archivo está vacío — las actualizaciones de ese tipo se ignoran, y la plataforma no ejecuta nada para ellas. Así que mantén solo los manejadores que realmente necesitas: cada uno que agregas es otro tipo de actualización al que tu bot se despierta para procesar, y omitir los demás significa que Telegram no pone en marcha tu código para actualizaciones que solo descartarías de todos modos.

Para generar un nuevo manejador, ejecuta npx tgcloud add handlers/<type> .

endpoints/ — el lado del servidor de tu Mini App. Cada archivo es una función que la Mini App llama por nombre, así que endpoints/getProfile.js responde a POST /api/getProfile. La plataforma verifica que la llamada provenga de un usuario de Telegram dentro de tu Mini App antes de que tu código se ejecute, y te entrega ese usuario. endpoints/ es plano, como handlers/. Ver Endpoints .

.tgcloud/ — estado local de la máquina gestionado completamente por la CLI, incluidas tus credenciales guardadas. Se ignora en git, y nunca debes leer ni escribir en él manualmente — usa los comandos de la CLI en su lugar.

El sistema de módulos

En tiempo de ejecución, un módulo puede ver exactamente dos cosas: el SDK de la plataforma y los otros módulos de tu proyecto. No hay paquetes npm, no hay sistema de archivos, y no hay red excepto a través de fetch del SDK.

Importa tus propios módulos de la forma en que lo harías en cualquier proyecto JavaScript — por ruta relativa, con la extensión .js — y el SDK por su nombre:

// tgcloud/handlers/message.js
import { users } from '../schema.js';              //  el módulo de esquema
import { addItem } from '../lib/cart.js';          //  un módulo de lib
import { format } from '../lib/internal/fmt.js';  //  módulo de lib anidado
import { db, api, fetch } from 'sdk';              //  el SDK de la plataforma
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        //  un vecino
import { users } from '../schema.js';

Una importación relativa debe terminar en .js y no puede salir de tgcloud/:

import { users } from '../schema';       //  agrega el .js
import config from '../../config.js';    //  fuera de tgcloud/
import { z } from 'zod';                 //  sin paquetes npm

Cada módulo también tiene un nombre — su ruta dentro de tgcloud/ sin la extensión, como schema, lib/cart o handlers/message. Ese nombre es lo que npx tgcloud run y la salida de la CLI usan, y un módulo también puede ser importado por él (from 'lib/cart', sin extensión). Prefiere importaciones relativas en tu código. Son lo que tu editor entiende — saltar a la definición, autocompletado, renombrar.

Lo que está disponible en tiempo de ejecución son exactamente dos cosas: sdk y sus submódulos (sdk/db, sdk/api, sdk/fetch) — toda la superficie de la plataforma, ver El SDK — y tus propios módulos en tgcloud/. Esa es la lista completa. Si tu código importa algo más, no se resolverá. Esta restricción es lo que mantiene los módulos rápidos de cargar y seguros de ejecutar.

Manejadores

Un manejador es un módulo en handlers/ cuya exportación por defecto la plataforma invoca cuando llega una actualización coincidente.

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

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

Un manejador recibe el payload de la actualización como su argumento — la plataforma desempaqueta la Update de Telegram por ti. handlers/message.js recibe el Message (es decir, update.message); handlers/callback_query.js recibe el CallbackQuery ; y así sucesivamente. El segundo argumento del manejador es un objeto de contexto por invocación, ctx. Lleva la Update cruda como ctx.update — úsalo cuando necesites algo fuera del payload, como update_id.

Un manejador puede ser async (generalmente lo es) y puede retornar un valor. Accede a la Bot API, la base de datos y el HTTP saliente a través del SDK.

No necesitas una actualización real para probar un manejador. npx tgcloud run lo ejecuta en la plataforma con el payload que suministras y tu código local actual:

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

El argumento es el payload — el mismo objeto que tu manejador recibe — en JSON5. Para suministrar el ctx del manejador (su segundo argumento), agrega --ctx, por ejemplo --ctx '{ update: { update_id: 1 } }'. Esto se ejecuta contra tus archivos locales, así que puedes probar cambios antes de desplegarlos. Ver run .

Endpoints

Un endpoint es un módulo en endpoints/ que tu Mini App llama — el lugar para cualquier cosa que el frontend no pueda hacer por sí mismo, como leer y escribir la base de datos, llamar a la Bot API, o hablar con servicios externos con tus secretos.

// 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 no encontrado', { userId: user.id });
  return profile;
}

El primer argumento, input, es el objeto JSON que la Mini App envió. El segundo, ctx, lleva ctx.initData — los datos de inicialización de la Mini App (user, chat, start_param, auth_date, …) , ya verificados por la plataforma. Tiene la misma forma que Telegram.WebApp.initDataUnsafe en el cliente, pero aquí puedes confiar en él. Una llamada sin datos de inicialización válidos se rechaza con 401 antes de que tu código se ejecute, así que ctx.initData.user es realmente la persona que abrió tu Mini App. Los endpoints solo pueden ser llamados de esta manera — desde la Mini App de tu bot, por un usuario de Telegram — así que no hay API pública que proteger y no hay inicio de sesión que construir.

Lo que sea que el endpoint retorne se envía de vuelta como JSON. Para rechazar una llamada, throw new EndpointError(description, parameters?). La Mini App recibe la descripción y el objeto parameters tal como están. Si necesitas ramificarte según el tipo de error en el frontend, pon un código en parameters — { code: 'OUT_OF_STOCK' }. Cualquier otra excepción se reporta como un error interno genérico, y el mensaje real solo va a tus logs, así que los internos nunca se filtran al cliente.

Llamando a un endpoint desde la Mini App

La Mini App llama a un endpoint por nombre con Telegram.WebApp.Serverless.call del SDK oficial de Mini Apps , telegram-web-app.js — el script que tu Mini App ya incluye. Envía los datos de inicialización por ti y llama de vuelta con un error o el resultado del endpoint:

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — la descripción; err.parameters — lo que llevó tu EndpointError
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

La entrada es opcional y debe ser un objeto plano. Sin ella, la llamada es Serverless.call('getProfile', callback).

err.type es 'ENDPOINT_ERROR' cuando tu endpoint arrojó un EndpointError. Cualquier otro valor significa que la plataforma rechazó la llamada o que el endpoint falló inesperadamente, por ejemplo 'UNAUTHORIZED' cuando faltan los datos de inicialización o son inválidos.

Los nombres de los endpoints son como identificadores de JavaScript — letras, dígitos y _, sin empezar con un dígito (getProfile, place_order). Genera uno con npx tgcloud add endpoints/<name> , y pruébalo sin la Mini App con run , pasándole tú mismo los datos de inicialización:

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

¿Qué se despliega?

Cuando ejecutas npx tgcloud push, la CLI recopila cada archivo .js bajo schema.js, lib/, handlers/ y endpoints/, y envía ese conjunto exacto como el espacio de módulos de tu proyecto — junto con la carpeta de compilación de la Mini App, si tgcloud.jsonc nombra una (ver abajo). Cualquier cosa presente en la nube pero ausente de tu proyecto se elimina, por lo que el estado desplegado siempre refleja tu carpeta — incluidas las eliminaciones. Nada fuera de tgcloud/ se despliega como código — el resto de la raíz del proyecto es tuyo. Dentro de tgcloud/, un archivo .js que no esté en una de esas ubicaciones (tgcloud/util.js, tgcloud/helpers/x.js) se marca, para que no se despliegue silenciosamente. Markdown, archivos ocultos y .tgcloud/ nunca se despliegan.

Frontend de Mini App

La raíz del proyecto es tuya, y es el lugar natural para el frontend de tu Mini App — construido con React, Vue, Svelte o HTML plano, por Vite o cualquier otro empaquetador. Serverless aloja su salida de compilación junto a tu bot. Un push despliega el código y los archivos estáticos juntos, y la aplicación se sirve en https://app<app_id>.tgcloud.ai/ (push imprime la dirección exacta — úsala como la URL de tu Mini App en @BotFather ).

Denuncia de abuso. Las Mini Apps en tgcloud.ai son publicadas por desarrolladores independientes. Para denunciar contenido ilegal o abuso en un subdominio app<app_id>.tgcloud.ai, envía un correo a [email protected] .

Para empezar, crea el frontend primero, con la herramienta que prefieras, luego agrega Serverless encima:

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

El creador de proyectos nunca sobrescribe tus archivos. Agrega la carpeta tgcloud/ junto al frontend, reconoce la carpeta de compilación y agrega un script npm run deploy que compila la app y la despliega junto con el bot.

Lo que se aloja se establece en tgcloud.jsonc, el archivo de configuración del proyecto en la raíz del proyecto:

{
  "$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" es requerido. Es la carpeta de compilación, relativa a la raíz del proyecto. Las otras claves de "static" configuran cómo se sirve (abajo). init crea tgcloud.jsonc para ti. Cuando reconoce el frontend, también rellena "source":

Frontend init establece "source" a
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular nada — configura una exportación estática primero, luego establece "source" a su carpeta tú mismo

Con cualquier otra configuración, establece tú mismo "source" a tu carpeta de compilación.

La línea $schema le da a tu editor autocompletado y validación para cada clave. El archivo es JSON con comentarios y comas finales permitidas. Si otra herramienta tuya necesita leerlo como JSON plano, nómbralo tgcloud.json en su lugar — entonces debe ser JSON estricto.

Compila antes de desplegar. push sube la carpeta exactamente como está en el disco — excepto archivos y carpetas cuyos nombres empiezan con un punto, que se omiten — y nunca ejecuta tu compilación, así que un simple npx tgcloud push después de editar el frontend desplegaría la compilación anterior. Usa npm run deploy (ejecuta tu script build, luego tgcloud push) , o compila tú mismo primero.

Las opciones de servicio:

  • spa — sirve index.html para rutas que no existen y no tienen extensión de archivo, así un router del lado del cliente funciona al recargar. Las rutas con extensión aún obtienen 404.
  • immutable — patrones para archivos con hash de contenido ( /assets/* de Vite) que los navegadores pueden cachear durante un año. Úsalo solo para archivos con hash de contenido. Cualquier otro archivo que coincida aquí se quedaría obsoleto en los navegadores durante un año, y no hay forma de recuperarlo.
  • headers — cabeceras de respuesta para las rutas que una regla coincide (una ruta exacta, o un prefijo que termina en /*). Un valor null elimina una cabecera.
  • redirects — reglas de redirección, donde la primera coincidencia gana. Un archivo existente siempre vence a una regla. status es 301 a menos que lo establezcas.
  • "static": "dist" — un atajo para { "source": "dist" } cuando no necesitas nada más.
  • "static": false — elimina el sitio desplegado en el siguiente push. Eliminar la clave en su lugar solo detiene que este proyecto gestione archivos estáticos, y lo que esté desplegado permanece.

El sitio se sirve desde la raíz de su propio host, por lo que la base (/) predeterminada de tu empaquetador es correcta. Telegram Web abre las Mini Apps en un iframe, así que no agregues X-Frame-Options ni una política restrictiva frame-ancestors.

El mismo host responde a tus endpoints bajo /api/ — ahí es donde va Telegram.WebApp.Serverless.call('getProfile', …). No pongas archivos propios bajo /api/ en la carpeta de compilación.

La base de datos

Cada bot obtiene su propia base de datos — un almacén basado en SQLite que persiste entre invocaciones y está disponible para cada módulo a través de db. Describes tus tablas en schema.js con un DSL pequeño y tipado; las lees y escribes con un constructor de consultas fluido; y las haces evolucionar con migraciones revisadas.

¿Conoces Drizzle ORM ? Entonces ya sabes cómo hablar con la base de datos aquí. El DSL de esquema y el constructor de consultas siguen a Drizzle — los constructores de columnas, select().from().where(), los operadores, onConflictDoUpdate, .returning(), y la etiqueta sql se comportan todos como esperarías, así que leer y escribir datos es la API familiar que ya usas. Simplemente la importas de sdk/db, y un par de especificidades de la plataforma (notablemente sin claves foráneas ) se señalan donde surgen.

Declaración de tablas

Las tablas son exportaciones nombradas en schema.js. Llamar a table() construye una descripción en tiempo de carga (no toca la base de datos); la plataforma descubre las tablas exportadas cuando despliegas schema.js y migra la base de datos para que coincida.

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 es el nombre de la tabla SQL;
  • columns es un mapa de propiedad JS → definición de columna;
  • extras es una función de callback opcional (t) => ({ … }) donde t expone las columnas (t.created es una referencia a esa columna) — declara índices y restricciones a nivel de tabla aquí.

Tipos de columna

Fábrica Tipo SQLite Notas
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB lee/escribe Uint8Array
boolean() INTEGER almacenado como 0/1, leído como true/false
json() TEXT auto JSON.stringify / JSON.parse

El argumento del nombre de columna es opcional — omítelo y se usa la clave JS. La opción mode controla cómo los valores se convierten entre SQLite y JavaScript.

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

boolean() y json() son atajos para integer(name, { mode: 'boolean' }) y text(name, { mode: 'json' }).

Un blob() lee y escribe un Uint8Array — el runtime no tiene Node Buffer (y un Buffer es una subclase de Uint8Array, por lo que este es el tipo base portátil). El mode de arriba rige solo cómo se codifica un valor, independientemente del tipo de almacenamiento de la columna: así blob('col', { mode: 'json' }) es el contraparte BLOB de json() — la misma codificación JSON, mantenida en una columna BLOB en lugar de TEXT.

EN RESUMEN: blob() usa Uint8Array. mode controla la codificación, no el almacenamiento, así que blob(..., { mode: 'json' }) almacena JSON como BLOB, mientras que json() lo almacena como TEXT.

Modificadores de columna

Los modificadores de columna se encadenan a una columna.

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('reemplazado por login')   // marca la columna para su eliminación

.default() en una columna json() codifica el valor por ti; un valor predeterminado sql`…` se pasa verbatim. .deprecated() es terminal — ver Migraciones .

Índices y restricciones

Los índices y restricciones se declaran en la función de callback extras, donde las columnas están en ámbito.

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

Los modificadores a nivel de tabla se encadenan después de table(...): .strict(), .withoutRowid(), .deprecated('razón').

Sin claves foráneas

El runtime se ejecuta con PRAGMA foreign_keys apagado. Una clave foránea declarada sería silenciosamente inerte — sin cascadas, sin protección de huérfanos — lo que es peor que no tener ninguna, así que el DSL lo hace imposible. Es decir, .references() y foreignKey() a nivel de tabla lanzan un error al declararse, y un esquema que las usa no se desplegará.

Debes modelar las relaciones con columnas simples (userId: integer('user_id')) y aplicar la integridad en el código de tu aplicación: inserta padres antes que hijos, elimina hijos antes que padres, maneja errores adecuadamente y barre los huérfanos con un LEFT JOIN … WHERE parent.id IS NULL cuando necesites hacerlo.

La ausencia de claves foráneas es una restricción deliberada, no un descuido. Tenla en cuenta desde el principio al planificar tu bot.

Consultas

db es un constructor de consultas fluido. Cada consulta es asíncrona — los métodos terminales (.all(), .get(), .values(), .run()) retornan Promesas, así que siempre usa 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 las filas
await db.select().from(todos).where(eq(todos.id, 1)).get();   // primera fila o null
await db.select().from(todos).values();                       // filas 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();

// proyección personalizada: { alias: columnaRef | expresiónSql | agregado }
await db.select({ id: todos.id, title: todos.text, n: count() })
  .from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();

// conteo de filas — una ayuda, no un terminal del constructor:
await db.$count(todos);                        // todas las filas
await db.$count(todos, eq(todos.done, false)); // con un filtro
  • Encadenable: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Terminales: .all(), .get(), .values().
  • Cuenta filas con db.$count(tabla, donde?) (o count() en una proyección).

Insertar, actualizar, eliminar:

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

Nota que una inserción en lote es una sola sentencia, por lo que está limitada por el límite de variables de SQLite (filas × columnas); si lo excedes, la inserción falla — divídela tú mismo.

Operadores se importan 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) con múltiples argumentos es lo mismo que and(a, b). El segundo argumento de una comparación es un valor por defecto, pero puede ser otra columna o sql`…` — por ejemplo, eq(a.x, b.y). Los agregados (count/sum/avg/min/max) son fragmentos sql para una proyección .select({ … }).

SQL crudo — cuando el constructor no es suficiente, baja a SQL crudo. El modo se elige por método: db.run para escrituras, db.all para muchas filas, db.get para una fila, db.values para filas como arrays posicionales.

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 leche']]

Una escritura se resuelve en un resultado de ejecución — rowsAffected, lastInsertRowid, y rows, que llevan una cláusula RETURNING si el SQL la tenía. Usa db.values cuando una consulta proyecta dos columnas del mismo nombre (un join como SELECT u.id, o.id): db.all clavea filas por nombre de columna, así que el segundo id sobrescribiría el primero.

La etiqueta sql`…` convierte ${value} en un parámetro enlazado y ${table.column} en un identificador, y empalma fragmentos sql anidados. Usa sql.raw('…') para un literal sin parámetros.

Las consultas crudas no están enlazadas a una tabla, así que sus filas vuelven sin conversión de modo — un booleano es 0/1, una columna JSON es una cadena, una marca de tiempo es un número. Solo el constructor enlazado a tabla convierte valores.

Migraciones

Tu base de datos cambia a medida que tu bot crece. La plataforma mantiene eso seguro separando el despliegue de código del cambio de datos, y clasificando cada cambio de esquema por cuán riesgoso es.

Desplegar nunca toca la base de datos. Cuando ejecutas npx tgcloud push de un schema.js modificado, la plataforma registra el nuevo esquema y te dice cómo cambiaría la base de datos — pero no aplica nada:

npx tgcloud push       # despliega schema.js; reporta cambios pendientes en DB
npx tgcloud migrate    # revisa y aplícalos

npx tgcloud migrate calcula la diferencia entre tu esquema y la base de datos en vivo y te guía paso a paso con un contador [N/M] en ejecución:

  • Un resumen breve de todos los cambios pendientes.
  • Cambios seguros, aplicados juntos en un solo paso, tras tu confirmación.
  • Advertencias (eliminaciones, operaciones lentas), una a la vez, cada una confirmada por separado.
  • Cambios manuales, mostrados con una razón y una acción sugerida, no aplicados automáticamente.
  • Objetos no documentados (en la base de datos pero no en tu esquema), mostrados para conciencia.

Termina con un resumen: aplicados, omitidos, esperando corrección manual, o no en el esquema.

Ver migrate para las banderas (--dry-run, --safe, --yes, --local).

Eliminar cosas

Eliminar una tabla o columna de schema.js no la elimina — eso haría que una eliminación accidental fuera catastrófica. Para eliminar algo, márcalo como obsoleto:

// eliminar una columna
text('email').deprecated('reemplazado por login')

// eliminar una tabla completa
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('sin uso');

En la siguiente migrate, el objeto obsoleto aparece como una eliminación de estado de advertencia que confirmas individualmente. Una vez que se va, elimina la declaración.

Cambiar el tipo de una columna

Los cambios de tipo son manuales — SQLite no siempre puede hacerlos in situ, y la coerción de valores existentes es una decisión de juicio. migrate mostrará el cambio y su razonamiento; realízalo tú mismo con SQL crudo (db.run(...)) , típicamente creando una nueva columna o tabla, copiando datos y luego intercambiando.

El SDK

En tiempo de ejecución, un módulo tiene una librería: sdk. Agrupa las tres cosas que un backend de bot necesita — una base de datos, la Telegram Bot API y HTTP saliente — sin nada que instalar y sin credenciales que configurar. La base de datos (db) se cubre en La base de datos ; esta sección cubre api, fetch, y la global console.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // toda la superficie
// o desde submódulos:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Import Qué es
db Base de datos — constructor de consultas y DSL de esquema → La base de datos
api Telegram Bot API — api.sendMessage(...) → abajo
fetch HTTP saliente → abajo
InputFile Un archivo (bytes + nombre) para subir — funciona con api y fetch → Archivos
EndpointError Rechaza la llamada de una Mini App con un mensaje que puede mostrar → Endpoints

Importa tus propios módulos de proyecto por ruta relativa con la extensión .js (from '../schema.js', from '../lib/cart.js'). Ver El sistema de módulos .

La Bot API

api te proporciona toda la Telegram Bot API . Llama a cualquier método como api.<método>(parámetros). Cada método actual — y futuro — de la Bot API funciona sin necesidad de actualizar el SDK.

import { api } from 'sdk';

const me = await api.getMe();                            // → el resultado desempaquetado
await api.sendMessage({ chat_id: id, text: '¡Hola!' });
await api.editMessageText({ chat_id, message_id, text: 'Actualizado' });
await api.answerCallbackQuery({ callback_query_id, text: 'Hecho' });

El envoltorio de respuesta está desempaquetado. La Bot API normalmente envuelve los resultados en { ok: true, result: … }. api devuelve directamente el result — getMe() se resuelve en el objeto de usuario, no en un envoltorio. Los parámetros usan los nombres snake_case propios de la Bot API (chat_id, message_id, reply_markup, …).

Los fallos lanzan BotApiError. Cuando la Bot API devuelve { ok: false }, la llamada lanza un BotApiError en lugar de retornar un valor falso, así que no puedes ignorarlo accidentalmente. El error lleva .code (el error_code de la Bot API) , .description (el mensaje legible por humanos) , .method (qué método falló) , y .parameters (datos adicionales como retry_after en un 429, o migrate_to_chat_id). Captúralo para manejar un fallo esperado y relanza el resto:

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = el mensaje ya no existe; está bien aquí.
  } else {
    throw e;
  }
}

Archivos

Trabaja con bytes de archivos, no solo con file_id — sube archivos nuevos y descarga los existentes.

Subir — InputFile. Envuelve bytes crudos en un InputFile (un contenedor plano: bytes, un nombre de archivo y un type MIME opcional) y pásalo donde un método de Bot API espere un archivo:

import { api, InputFile } from 'sdk';

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

// los archivos anidados dentro de los parámetros también funcionan — por ejemplo, un á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' }) },
]});

Los bytes se transmiten a Telegram como una carga multipart real — el archivo no se almacena en búfer completo en una sola llamada a la plataforma — pero los límites de la Bot API de Telegram aún se aplican: un bot puede actualmente subir hasta 50 MB por archivo (y 10 MB para una foto).

Descargar — por file_id. Dos ayudas extraen los bytes de un archivo. La plataforma resuelve el file_id por ti (ejecuta getFile y recupera el contenido en segundo plano) — así que nunca tocas el token del bot ni una URL de descarga:

const bytes = await api.getFileContent(file_id);          // → Uint8Array (todo el archivo)

const { file, body } = await api.getFileStream(file_id);  // transmítelo en su lugar
file.file_unique_id;                                      // la información de getFile
for await (const chunk of body) { /* Uint8Array */ }      // o búferalo: await stream.bytes()

Las descargas de getFile de la Bot API de Telegram están limitadas a 20 MB, así que ese es el límite para ambas ayudas.

HTTP

fetch es un cliente tipo fetch para llamar al mundo exterior — APIs de terceros, webhooks, cualquier cosa vía 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 respuesta refleja la plataforma web: res.status, res.statusText, res.ok (true para 200–299) , res.url, res.headers (.get(), .has(), .keys(), .entries()) , y lectores de cuerpo await res.json() / await res.text().

También puedes leer el cuerpo incrementalmente como una corriente — for await (const chunk of res.body) { … } — que es cómo consumes eventos enviados por servidor o salida token por token de APIs de IA.

Las ayudas de cuerpo establecen el Content-Type correspondiente por ti:

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

Los cuerpos de solicitud también pueden llevar archivos y bytes crudos: pasa un Uint8Array, un InputFile, o un FormData (para multipart/form-data) como body, y los bytes se transmiten — así puedes subir a una API de terceros de la misma forma que envías archivos a 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', 'desde mi bot');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });

De lo contrario, se comporta como el fetch estándar que ya conoces, con dos restricciones:

  • La respuesta se lee como texto — await res.json() / await res.text(), o se transmite trozo a trozo con res.body.
  • La respuesta total está limitada a 30 MB. Ese límite cubre toda la respuesta — transmitir con res.body te permite procesar un cuerpo grande incrementalmente, pero no aumenta el límite.

Registro

La global console estándar está disponible — nada que importar, simplemente está ahí como en cualquier JavaScript. Su salida se captura y se muestra con npx tgcloud run, lo que la convierte en tu herramienta principal de depuración durante el desarrollo.

console.log('procesando', { chatId: id });   // log / debug — plano
console.info('iniciado');                     // info  — azul
console.warn('tasa limitada');                // warn  — amarillo
console.error(err);                          // error — rojo, con un rastreo de pila

Cada línea se etiqueta con el [archivo:línea] de donde proviene. console.error y console.trace agregan un rastreo completo, mientras que console.warn no lo hace. Cuando ejecutas npx tgcloud run un módulo, estas líneas se imprimen con un prefijo de color por nivel, el tiempo transcurrido desde que comenzó la ejecución y el origen — ver run .

Interfaz de línea de comandos

tgcloud es el puente entre tu carpeta de proyecto y la nube. Genera proyectos, te muestra qué cambió, despliega, ejecuta módulos y aplica migraciones de base de datos. Necesita Node.js 18 o superior. Dos formas de obtenerlo:

# Recomendado — crea un proyecto con la CLI instalada en él:
npm create @tgcloud/bot example_bot

# O instala la CLI globalmente e inicializa una carpeta vacía:
npm install -g @tgcloud/cli
tgcloud init

El paquete npm es @tgcloud/cli, y el comando que instala es tgcloud. Cada comando funciona desde cualquier subcarpeta de tu proyecto.

Comando Propósito
init Genera un nuevo proyecto en la carpeta actual
add Genera un nuevo módulo (un manejador, un endpoint o un módulo de lib)
login Vincula el proyecto a un bot (guarda el token)
status Muestra qué cambió localmente vs. la nube
diff Muestra los cambios línea por línea
push Despliega módulos cambiados y la compilación de la Mini App a la nube
migrate Aplica cambios de esquema a la base de datos
run Ejecuta un módulo en la plataforma sin desplegar
fetch Verifica qué está desplegado, sin tocar los archivos locales
pull Trae los archivos locales a línea con la nube
reset Descarta los cambios locales; restaura desde el estado de la nube
webhook Inspecciona y resincroniza el webhook gestionado por la plataforma
completion Imprime un script de completado de shell (bash/zsh/fish)
upgrade Mueve la disposición de un proyecto pre-0.2.0 a tgcloud/

Autenticación

Un proyecto está vinculado a un bot por su token, que tiene la forma app<id>:<secret>. La parte app<id> es pública y puede imprimirse; el secreto nunca aparece en logs ni errores.

El token se resuelve en este orden:

  1. Variable de entorno TGCLOUD_TOKEN — para CI; nunca se escribe en disco.
  2. .tgcloud/credentials — escrito por npx tgcloud login .
  3. Ni uno ni otro → un error que te indica que uses npx tgcloud login.

La CLI nunca pide un token en medio de un comando. Iniciar sesión es siempre el paso explícito login. Si un token guardado se vuelve inválido, la CLI lo elimina y te pide que inicies sesión de nuevo.

init

npx tgcloud init

Genera un nuevo proyecto en el directorio actual: la carpeta tgcloud/ con schema.js, lib/, handlers/ y un manejador inicial, además de tgcloud.jsonc, AGENTS.md, docs/, y la carpeta de estado .tgcloud/. Ejecútalo en la raíz de un proyecto de frontend existente también — agrega la carpeta tgcloud/ y la documentación, y nunca sobrescribe tus archivos. Si reconoce el frontend, también apunta tgcloud.jsonc a la carpeta de compilación y agrega un script deploy (compilar, luego push) a tu package.json — solo si ya no tienes un script deploy. Ver Frontend de Mini App .

init se niega a crear un proyecto dentro de otro proyecto. Volver a ejecutarlo en la raíz de un proyecto está bien y solo agrega lo que falte.

add

npx tgcloud add <target>

Genera un solo módulo nuevo, conectado y listo para editar — un manejador, un endpoint o un módulo de lib/.

npx tgcloud add handlers/callback_query   # un nuevo manejador de actualización
npx tgcloud add endpoints/getProfile      # un nuevo endpoint para la Mini App
npx tgcloud add lib/cart                  # un nuevo módulo compartido

Nota que add nunca sobrescribe un archivo existente.

El <target> es la ruta del módulo (un .js final opcional). Para handlers/, el nombre debe ser un tipo de actualización de Telegram. La plataforma anuncia el conjunto válido, así que un nombre inválido se rechaza de antemano. handlers/ y endpoints/ son planos, mientras que lib/ puede ser anidado (lib/payments/stripe).

El nombre del módulo es requerido. Dar solo el directorio es un error — pero uno útil: para handlers/ enumera los tipos de actualización que aún no tienes, para que puedas copiar uno.

$ npx tgcloud add handlers
Error: Especifica un nombre, por ejemplo "handlers/callback_query". Tipos de handlers/ disponibles: callback_query, inline_query, chat_member, …

<Tab> completado ofrece el mismo conjunto — ver completion .

El archivo generado tiene una export default en vivo, así que el manejador está activo tan pronto como despliegas — no hay nada que descomentar. Despliega el nuevo módulo con push .

login

npx tgcloud login

Solicita tu token de acceso a la CLI — de @BotFather → tu bot → Serverless → CLI Access → Access token, un token separado de tu token de API de bot — lo valida contra la plataforma y lo guarda en .tgcloud/credentials.

login es el único comando que pide un token, y requiere una terminal interactiva. En CI, usa TGCLOUD_TOKEN en su lugar.

status

npx tgcloud status

Muestra, por archivo, qué ha cambiado entre tu directorio de trabajo y la copia desplegada: modificado, nuevo, eliminado, sin cambios. Funciona sin conexión, comparando con el estado de la nube desde tu último push, pull o fetch. También advierte sobre archivos .js extraños dentro de tgcloud/ que no se desplegarán.

Para un proyecto con una Mini App, status también muestra qué cambió en su carpeta de compilación desde el último despliegue.

diff

npx tgcloud diff

Al igual que status, pero muestra las diferencias reales línea por línea para los módulos modificados. También sin conexión.

push

npx tgcloud push [files...]

Despliega tu proyecto en la nube en un lote atómico.

Sin argumentos, despliega todo el proyecto, y el estado desplegado se hace un reflejo exacto de tu carpeta — los módulos que eliminaste localmente se eliminan en la nube.

Con argumentos de archivo o directorio (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/) , despliega solo los cambios en esos archivos. Un push dirigido nunca elimina otros módulos. Un objetivo puede ser escrito como la ruta del archivo (tgcloud/handlers/message.js) , la ruta dentro de tgcloud/ (handlers/message.js) o el nombre del módulo (handlers/message) — todos significan el mismo módulo.

Su única opción es --force — para omitir la verificación de concurrencia y sobrescribir lo que sea que haya en la nube. Úsalo solo cuando estés seguro (ver Mantenerse sincronizado ).

Si tgcloud.jsonc nombra una carpeta estática, un push completo la despliega en el mismo lote — solo los archivos que cambiaron se suben — e imprime la dirección del sitio. Compila primero, porque push nunca ejecuta tu compilación (npm run deploy hace ambas cosas). npx tgcloud push dist/ despliega solo los archivos estáticos, y un push que nombra solo módulos los deja sin tocar.

Después de un despliegue, si schema.js cambió y la base de datos está desincronizada, push imprime un resumen de los cambios pendientes y sugiere npx tgcloud migrate. Nunca los aplica por sí mismo.

migrate

npx tgcloud migrate

Aplica tus cambios de esquema a la base de datos. Calcula la diferencia entre schema.js y la base de datos en vivo, luego te guía paso a paso con un contador en ejecución [N/M]:

  • Un resumen breve de todos los cambios pendientes.
  • Cambios seguros, aplicados juntos en un solo paso tras tu confirmación.
  • Advertencias (eliminaciones, operaciones lentas), una a la vez, cada una confirmada por separado.
  • Cambios manuales, mostrados con una razón y una acción sugerida, no aplicados automáticamente.
  • Objetos no documentados (en la base de datos pero no en tu esquema), mostrados para conciencia.

Termina con un resumen: aplicados, omitidos, esperando corrección manual, o no en el esquema.

Ver Migraciones para el modelo.

Opciones: --dry-run (imprime todo, no aplica nada) , --safe (aplica automáticamente cambios seguros, omite advertencias) , --yes (aplica automáticamente cambios seguros y todas las advertencias, omite manuales — úsalo con cuidado) , --local (compara contra tu schema.js local en lugar del desplegado). Sin bandera, migrate requiere una terminal y falla en un entorno no interactivo en lugar de adivinar.

run

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

Ejecuta un manejador o un endpoint en la plataforma sin desplegar, usando tus archivos locales actuales. Este es el bucle interno rápido para probar lógica.

  • <module> — un nombre desnudo (buscado bajo handlers/ y endpoints/) o una ruta como handlers/message. Un nombre desnudo que existe en ambos directorios debe escribirse con su directorio.
  • [args] — el payload pasado al manejador, escrito en JSON5 para que puedas omitir las comillas de las claves. Es el objeto de tipo de actualización que recibe tu manejador (por ejemplo, un Mensaje para handlers/message) , o la input de un endpoint, y debe ser un objeto — no un array o una cadena desnuda.
  • --ctx <json5> — el objeto de contexto del módulo (su segundo argumento), también JSON5. Úsalo para suministrar lo que tu código lee de ctx — la actualización cruda para un manejador, --ctx '{ update: { update_id: 1 } }', o los datos de inicialización para un endpoint, --ctx '{ initData: { user: { id: 1 } } }'. run no lo verifica, ya que estás probando tu lógica, no las comprobaciones de la plataforma.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hola" }'

La plataforma ejecuta el módulo contra el espacio de módulos ensamblado desde tu proyecto local (así que el código lib/ modificado localmente también se usa) y retorna el valor de retorno, cualquier cosa registrada con console.*, y el tiempo transcurrido. Lee argumentos grandes de un archivo con npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Verifica qué está desplegado actualmente, sin tocar tus archivos de trabajo. Después de eso, status y diff comparan con el último estado de la nube. Útil para ver un conflicto antes de decidir cómo resolverlo.

pull

npx tgcloud pull

Trae tus archivos locales a línea con la nube actualizando tus archivos de trabajo al estado desplegado.

pull nunca escribe la carpeta de compilación de tu Mini App.

reset

npx tgcloud reset

Descarta tus cambios locales y restaura los módulos desde el último estado de la nube conocido. Úsalo para desechar un experimento. La carpeta de compilación de la Mini App no se toca.

webhook

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

Telegram entrega actualizaciones a tu bot a través de un webhook, que la plataforma gestiona por ti — nunca lo apuntas a ningún lugar manualmente. npx tgcloud webhook muestra su estado actual: la URL, la lista de allowed_updates, cuántas actualizaciones están pendientes, el último error de entrega (si lo hay) , y si está sincronizado con tus manejadores desplegados.

“Sincronizado” significa que el webhook apunta a la plataforma y sus allowed_updates coinciden con los manejadores que has desplegado — así que Telegram entrega exactamente los tipos de actualización que manejas, y nada más. Desplegar un nuevo manejador (o eliminar uno) puede dejar el webhook desincronizado hasta que se refresque; npx tgcloud status también señala esto.

npx tgcloud webhook sync lo arregla — re-apunta el webhook a la plataforma y reconstruye allowed_updates de tus manejadores desplegados. Agrega --drop-pending para descartar las actualizaciones que Telegram ya había puesto en cola antes de la sincronización (de lo contrario, se entregan una vez que el webhook vuelve a estar saludable).

completion

Nota: el autocompletado de tabulación solo funciona cuando un tgcloud desnudo está en tu PATH — así que instálalo globalmente (npm install -g @tgcloud/cli) o de lo contrario pon el binario en tu PATH. No puede conectarse a npx.

tgcloud completion <bash|zsh|fish>

Imprime un script de completado de shell a stdout. Habilítalo una vez, luego <Tab> completará comandos, banderas, directorios de módulos, los tipos de actualización de manejadores que aún no tienes, y tus módulos locales ejecutables.

# bash — necesita el paquete bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — asegúrate de que `autoload -U compinit && compinit` se ejecute en tu ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

Reinicia tu shell (o vuelve a cargar el archivo) después. Ejecutar tgcloud completion sin un shell imprime estas instrucciones de nuevo.

upgrade

npx tgcloud upgrade [--dry-run]

Migración única para proyectos creados antes de la CLI 0.2.0, que mantenían schema.js, lib/ y handlers/ en la raíz del proyecto. Los mueve a tgcloud/. Tu enlace con el bot se mantiene, y nada necesita ser redespnegado. --dry-run imprime los movimientos planeados sin tocar nada. Otros comandos no se ejecutan en la vieja disposición hasta que actualices.

Mantenerse sincronizado

Cada proyecto tiene una revisión monotónicamente creciente en la nube, incrementada en cada despliegue. La CLI recuerda la revisión con la que se sincronizó por última vez y la envía en cada push. Si la nube ha avanzado — porque otra máquina o colega desplegó — el push se rechaza en lugar de sobrescribir su trabajo silenciosamente, y la CLI ofrece tres caminos a seguir:

npx tgcloud fetch           # ver qué cambió en la nube, luego volver a verificar
npx tgcloud pull            # traer lo último a tus archivos de trabajo
npx tgcloud push --force    # sobrescribir el estado de la nube (peligroso)

Esta verificación de concurrencia optimista es la razón por la que puedes compartir un bot entre un equipo sin un proceso de despliegue paso a paso. Los comandos salen con un código no cero en caso de fallo — un despliegue rechazado, una migración fallida, un error de autenticación, un módulo que lanzó un error durante run — así que se componen limpiamente en scripts y pipelines de CI.