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
pushse detiene para que primero hagaspull— 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.jscon 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/yhandlers/en la raíz del proyecto. Ejecutanpx tgcloud upgradeuna vez para moverlos dentro detgcloud/.
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— sirveindex.htmlpara 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 obtienen404.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 valornullelimina una cabecera.redirects— reglas de redirección, donde la primera coincidencia gana. Un archivo existente siempre vence a una regla.statuses301a 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 etiquetasqlse comportan todos como esperarías, así que leer y escribir datos es la API familiar que ya usas. Simplemente la importas desdk/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?):
namees el nombre de la tabla SQL;columnses un mapa de propiedad JS → definición de columna;extrases una función de callback opcional(t) => ({ … })dondetexpone las columnas (t.createdes 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
modecontrola 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()usaUint8Array.modecontrola la codificación, no el almacenamiento, así queblob(..., { mode: 'json' })almacena JSON como BLOB, mientras quejson()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 columnajson()codifica el valor por ti; un valor predeterminadosql`…`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?)(ocount()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
migratepara 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
getFilede 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 conres.body. - La respuesta total está limitada a 30 MB. Ese límite cubre toda la respuesta — transmitir con
res.bodyte 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:
- Variable de entorno
TGCLOUD_TOKEN— para CI; nunca se escribe en disco. .tgcloud/credentials— escrito pornpx tgcloud login.- 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
addnunca 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 — vercompletion.
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.
logines el único comando que pide un token, y requiere una terminal interactiva. En CI, usaTGCLOUD_TOKENen 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 bajohandlers/yendpoints/) o una ruta comohandlers/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 parahandlers/message) , o lainputde 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 dectx— 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 } } }'.runno 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
tgclouddesnudo está en tuPATH— así que instálalo globalmente (npm install -g @tgcloud/cli) o de lo contrario pon el binario en tuPATH. No puede conectarse anpx.
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.