Telegram Serverless дозволяє запускати серверний код для вашого бота та міні-додатку (Mini App) безпосередньо на інфраструктурі Telegram — без необхідності налаштовувати сервери, підтримувати роботу контейнерів чи думати про масштабування. Ви пишете звичайні модулі JavaScript, розгортаєте їх однією командою, а Telegram запускає їх у швидкій ізольованій пісочниці V8, яка розташована поруч із Bot API та вбудованою базою даних.

Якщо ви колись підключали бота до VPS, хмарної функції або хостинг-панелі лише для того, щоб відповісти на команду /start, то це та частина роботи, яку вам більше не доведеться виконувати.

Чому безсерверні технології (Serverless)

Telegram-бот — це, по суті, програма, що реагує на оновлення. Традиційно вам потрібно було розміщувати цю програму десь, де вона завжди увімкнена, доступна та захищена, а потім підтримувати цей стан. Telegram Serverless повністю усуває цей рівень:

  • Жодної інфраструктури. Немає серверів, які потрібно орендувати, оновлювати чи відстежувати. Ваш код виконується за запитом і автоматично масштабується разом із ботом.
  • Облікові дані включені. Telegram Bot API, база даних на основі SQLite та вихідні HTTP-запити доступні кожному модулю відразу після встановлення — нічого не потрібно інсталювати чи налаштовувати облікові дані.
  • Швидке, ізольоване виконання. Кожне звернення виконується в легкому ізоляті V8, близько до власних систем Telegram, тому виклики до Bot API та вашої бази даних відбуваються швидко і надійно.
  • Справжній робочий процес розробника. Проєкт живе в папці на вашому комп’ютері під контролем версій. Ви редагуєте файли, бачите, що саме змінилося, розгортаєте все атомарно та оновлюєте схему бази даних за допомогою перевірених міграцій — так само, як ви вже працюєте з усім іншим.

Ментальна модель: як працює проєкт

Ви працюєте у трьох місцях, які чітко співвідносяться одне з одним:

Місце Що там знаходиться
Папка проєкту Модулі JavaScript — схема, спільний код, обробники оновлень, кінцеві точки (endpoints) міні-додатку — і, якщо є, фронтенд міні-додатку
Хмара Розгорнута копія цих модулів, база даних вашого бота та файли міні-додатку
CLI tgcloud Міст — він показує відмінності та синхронізує їх

Вам ніколи не потрібно використовувати SSH. Ви редагуєте файли локально, запускаєте npx tgcloud push, а платформа бере все інше на себе. Трафік вашого бота обробляється розгорнутими модулями; ваша база даних зберігає стан між викликами.

Проєкт має лише чотири типи коду, всі всередині однієї папки tgcloud/:

tgcloud/
├─ endpoints/  # функції, які викликає ваш міні-додаток
├─ handlers/   # функції, що реагують на типи оновлень Telegram
├─ lib/        # спільний код, який ви імпортуєте звідки завгодно
└─ schema.js   # таблиці вашої бази даних

Коли надходить оновлення — повідомлення, натискання кнопки, інлайн-запит — Telegram спрямовує його до відповідного обробника (handlers/message.js, handlers/callback_query.js, …) і викликає його стандартний експорт. Ця функція взаємодіє з Bot API та базою даних через SDK і повертає результат. Це весь цикл. Оновлення без відповідного обробника просто ігнорується, тому ви додаєте лише ті обробники, які вам потрібні.

Міні-додаток є необов’язковим. Якщо у вашого бота він є, його фронтенд живе в тому ж проєкті, поруч із tgcloud/. Serverless розміщує результати його збірки та запускає кінцеві точки endpoints/, які він викликає. Див. Фронтенд міні-додатку .

Швидка демонстрація

Ось повний робочий демонстраційний бот. Він відповідає на кожне повідомлення і запам’ятовує, скільки повідомлень він отримав від кожного чату.

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;

  // Вставте лічильник або збільште його, якщо цей чат уже має такий,
  // і отримайте рядок назад у тому ж запиті за допомогою .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: `Привіт! Я бачив від вас ${row.seen} повідомлення(ь).`,
  });
}

Розгорніть це:

npx tgcloud push       # завантажити модулі
npx tgcloud migrate    # створити таблицю `counters`

Це живий бот із постійним станом і без сервера. Все в ньому — api, db, DSL table() — описано в розділах нижче.

Serverless — це загальний бекенд для Telegram-ботів та міні-додатків, а не шаблон для одного типу програм. Він ідеально підходить для:

  • Розмовних AI-ботів, яким потрібно зберігати стан кожного користувача в базі даних.
  • Бекендів для міні-додатків, які зберігають дані користувачів і надають динамічний контент.
  • Ігор та інструментів — включаючи таблиці лідерів, вікторини та багато іншого.
  • Автоматизацій та інтеграцій, які викликають сторонні HTTP API та надсилають результати в чати.

Початок роботи

Це покрокове керівництво проведе вас від порожньої папки до живого бота, який відповідає на повідомлення і зберігає дані. Воно передбачає, що у вас встановлено Node.js версії 18 або новіше, а бот зареєстрований через @BotFather . До кінця ви використаєте кожну команду, необхідну для повсякденної роботи: push, migrate, run та status.

Перш за все, увімкніть Serverless. У @BotFather відкрийте свого бота → Serverless і увімкніть функцію. Це активує її для цього бота та розблокує токен доступу до CLI, обробники, бібліотеку та базу даних.

1. Створення проєкту

Найшвидший спосіб розпочати — скористатися творцем проєктів, який створює структуру проєкту та встановлює в нього CLI:

npm create @tgcloud/bot example_bot
cd example_bot

Аргумент — це цільова папка: передайте ., щоб розгорнути в поточній папці, або будь-який шлях. Це працює і в існуючій папці, ніколи не перезаписуючи файли, які у вас уже є.

Це дає вам проєкт, готовий до редагування:

example_bot/
├─ tgcloud/             # все, що запускає платформа
│  ├─ handlers/
│  │  └─ message.js     # стартовий обробник повідомлень (повертає ехо)
│  ├─ lib/              # ваші спільні модулі тут (порожньо на старті)
│  └─ schema.js         # ваші таблиці бази даних
├─ docs/
│  └─ tgcloud-sdk.md    # посилання на SDK (для вас та ваших AI-інструментів)
├─ AGENTS.md            # орієнтир для AI-асистентів програмування
├─ package.json
└─ tgcloud.jsonc        # конфігурація проєкту

Створені файли мають самодокументацію — кожен містить коментовані приклади того, що можна зробити далі.

CLI встановлюється в проєкт як локальна dev-залежність, тому ви запускаєте його через npx tgcloud <команда> (npx знаходить копію у вашому node_modules), або через ярлики npm run, які додаються в package.json (npm run deploy, npm run status). За замовчуванням глобального tgcloud у вашому PATH немає.

Ви також можете встановити його глобально — npm install -g @tgcloud/cli — якщо хочете вводити просто tgcloud з будь-якого місця. Це зручно для запуску tgcloud init у будь-якій порожній папці, і саме це потрібно для автодоповнення в командному рядку. У будь-якому випадку ви отримаєте однаковий проєкт.

Створюєте міні-додаток? Почніть з його фронтенду. Створіть його за допомогою вашого звичного інструменту, потім запустіть творець проєктів у цій папці. Див. Фронтенд міні-додатку .

2. Прив’язка бота

Кожен проєкт прив’язаний до одного бота. З’єднайте їх за допомогою login, який запитає ваш токен доступу до CLI ( @BotFather → ваш бот → Serverless → CLI Access → Access token — окремий токен від API-токена вашого бота ) і збереже його локально:

npx tgcloud login

Токен має формат app<id>:<secret>. CLI зберігає його в .tgcloud/, який ігнорується Git, і ніколи не виводить секретну частину. Login — це єдиний момент, коли вас про нього запитають. Див. Автентифікація , щоб дізнатися, як токени вирішуються в CI.

3. Огляд

Дві команди показують стан справ у будь-який момент, обидві повністю офлайн:

npx tgcloud status     # що змінилося локально порівняно з розгорнутою копією
npx tgcloud diff       # зміни рядок за рядком

Одразу після init все нове, і нічого ще не розгорнуто. status показує стартові файли, що чекають на завантаження.

4. Розгортання (Deploy)

Надішліть свої модулі в хмару:

npx tgcloud push

push завантажує кожен змінений модуль одним атомарним пакетом і оновлює ваш локальний запис того, що зараз зберігається в хмарі. Ваш бот активний: відкрийте його в Telegram і надішліть повідомлення — стартовий обробник поверне його назад.

Deploy ніколи не чіпає вашу базу даних. Надсилання коду та зміна схеми бази даних — це свідомо різні кроки, тому розгортання коду ніколи не здивує вас міграцією даних. Для цього наступний крок.

5. Додавання таблиці бази даних

Давайте зробимо так, щоб бот щось запам’ятав. Відкрийте tgcloud/schema.js і оголосіть таблицю:

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

Розгорніть схему, а потім застосуйте її до бази даних:

npx tgcloud push       # завантажує нову схему
npx tgcloud migrate    # створює таблицю `messages`

push повідомляє, що схема не синхронізована, і показує очікувану зміну, але нічого не застосовує. migrate проведе вас через зміни і, після вашого підтвердження, створить таблицю. Ця двоетапна модель (і те, що відбувається з ризикованішими змінами, як-от видалення) описана в розділі Міграції .

6. Зберігання та читання даних

Тепер використовуйте таблицю у своєму обробнику. Відредагуйте tgcloud/handlers/message.js:

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

export default async function (message) {
  // Зберегти це повідомлення.
  await db.insert(messages)
    .values({ chatId: message.chat.id, text: message.text })
    .run();

  // Підрахувати, скільки повідомлень ми зберегли для цього чату.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Збережено. Наразі від цього чату вже ${count} повідомлення(ь).`,
  });
}

Розгорніть оновлений обробник за допомогою npx tgcloud push, потім надішліть боту кілька повідомлень і спостерігайте, як зростає лічильник. База даних зберігає стан між викликами — це пам’ять вашого бота.

7. Тестування без розгортання

Вам не потрібно робити розгортання, щоб спробувати зміни. npx tgcloud run виконує обробник на платформі, використовуючи ваші локальні файли, без їх публікації:

npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "привіт" }'

Аргумент — це корисне навантаження (payload), яке отримує ваш обробник — для handlers/message це Message — записаний у форматі JSON5 (тому ви можете не брати ключі в лапки). Команда виводить усе, що обробник вивів через console.*, значення, яке він повернув, і час виконання. Це найшвидший спосіб ітерації логіки — жодного розгортання, жодного очікування реального повідомлення.

8. Підтримання синхронізації

Під час роботи кілька команд допомагають тримати локальний проєкт і хмару в актуальному стані: npx tgcloud status показує зміни, npx tgcloud push розгортає, npx tgcloud pull приводить локальний проєкт у відповідність до хмари, npx tgcloud fetch перевіряє, що розгорнуто, не торкаючись файлів, а npx tgcloud reset відкидає локальні зміни.

Якщо двоє людей (або дві машини) розгортають одного і того ж бота, платформа виявляє конфлікт, і push зупиняється, щоб ви могли спочатку зробити pull — ви ніколи не зможете мовчки перезаписати чиюсь чужу роботу. Див. Підтримання синхронізації .

Розробка з AI

Ви віддаєте перевагу розробці з AI-асистентом — або, можливо, єдиний програміст у вашій команді — це AI? Ви все одно можете створити бота. Ми зробили перший крок, щоб AI-агент почувався як вдома в проєкті: кожен новий проєкт створюється з AGENTS.md та довідником docs/tgcloud-sdk.md, які інструменти програмування для агентів читають автоматично.

Разом із маленьким автономним середовищем виконання — одне SDK, жодних зайвих пакетів npm — це дає асистенту впевнений старт у правилах, які загальний кодогенератор часто пропускає: відносні імпорти з .js, жодних зовнішніх ключів, кожен виклик db є асинхронним, один обробник на тип оновлення та двоетапний потік push/migrate.

Спробуйте:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # або Claude Code, Cursor, … — будь-який агент, який читає AGENTS.md

Потім просто запитайте звичайною мовою:

Напиши бота, який запам’ятовує список справ кожної людини — додавай пункт, коли вони надсилають текст, і показуй весь список, коли надсилають /list.

Асистент редагує schema.js та ваші обробники; ви перевіряєте, миттєво тестуєте зміну за допомогою npx tgcloud run, а потім випускаєте в роботу за допомогою npx tgcloud push та npx tgcloud migrate. AGENTS.md — це частина вашого проєкту; редагуйте його в міру зростання бота, щоб інструкції залишалися актуальними.

У дорозі з BotFather

Маєте лише телефон? Весь проєкт живе і в @BotFather — відкрийте бота → Serverless, і ви отримаєте все, чим керує CLI, прямо на сенсорному екрані:

  • Handlers (Обробники) — створюйте, редагуйте та тестуйте обробники оновлень; BotFather підтримує вебхук у синхронізації з вашими обробниками (той самий стан In sync / Out of sync, який повідомляє CLI).
  • Endpoints (Кінцеві точки) — функції, які викликає ваш міні-додаток.
  • Library (Бібліотека) — ваші спільні модулі lib/.
  • Database (База даних) — редагуйте schema.js у тому ж стилі Drizzle, переглядайте очікувані зміни та застосовуйте їх; кнопка Save робить розгортання.
  • Mini App Files (Файли міні-додатку) — фронтенд, який ваші користувачі відкривають у Telegram.
  • CLI Access — отримайте токен доступу до CLI тут, коли повернетеся до клавіатури.

Це один і той самий хмарний проєкт, тому ви можете почати обробник на телефоні, а пізніше зробити npx tgcloud pull на ноутбук — нічого не прив’язано до одного клієнта. Запуск обробника навіть показує його вивід у Console прямо в чаті, так само як npx tgcloud run .

Проєкти та модулі

Безсерверний проєкт — це звичайна папка під контролем версій. Все, що запускає платформа, знаходиться в одній папці, tgcloud/; решта проєкту — ваша: фронтенд міні-додатку, конфігурація, документація. Тут немає кроку збірки для бота, немає node_modules під час виконання та немає точки входу сервера.

Анатомія проєкту

example_bot/
├─ tgcloud/             # розгорнута частина — нічого більше
│  ├─ handlers/         # обробники оновлень — плоска структура, лише один рівень
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # функції, які викликає ваш міні-додаток — плоска структура
│  │  └─ getProfile.js
│  ├─ lib/              # спільні модулі; дозволені підпапки
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # схема бази даних — один файл
└─ .tgcloud/            # стан CLI, ігнорується Git

Лише .js файли всередині tgcloud/ — schema.js, lib/, handlers/, endpoints/ — розгортаються. Все інше — Markdown, файли конфігурації, папка .tgcloud/, все за межами tgcloud/ — залишається на вашому комп’ютері. Імена модулів є відносними до tgcloud/. Файл tgcloud/handlers/message.js — це модуль handlers/message, і це ім’я використовує npx tgcloud run.

Оновлюєте старий проєкт? Проєкти, створені до CLI 0.2.0, зберігали schema.js, lib/ та handlers/ у корені проєкту. Запустіть npx tgcloud upgrade один раз, щоб перемістити їх у tgcloud/.

schema.js — ваша база даних. Вона оголошує таблиці як іменовані експорти за допомогою Schema DSL і живе в tgcloud/ як один файл. Вона розгортається, як і будь-який інший модуль, але її розгортання ніколи не змінює базу даних — зміни схеми застосовуються окремо за допомогою npx tgcloud migrate. Див. База даних .

lib/ — спільний код, все, що ви хочете повторно використовувати в обробниках: чисті помічники, шари доступу до бази даних, форматування, інтеграції зі сторонніми сервісами. lib/ — це єдиний каталог, який може містити підкаталоги (lib/internal/util.js, lib/payments/stripe.js), тому ви можете організувати велику кодову базу, як вам зручно. Модулі в lib/ ніколи не викликаються платформою безпосередньо; вони існують для того, щоб їх імпортували обробники та інші модулі.

handlers/ — точки входу вашого бота. Кожен файл відповідає одному типу оновлення Telegram, і платформа спрямовує кожне вхідне оновлення до відповідного обробника:

Файл Обробляє
handlers/message.js Нові вхідні повідомлення
handlers/inline_query.js Нові вхідні inline запити
handlers/callback_query.js Нові вхідні callback-запити
… будь-який інший тип оновлення Bot API

handlers/ має плоску структуру — без підпапок. export default обробника — це функція, яку викликає платформа (див. Handlers ).

Тип оновлення обробляється тільки якщо файл обробника існує і не порожній. Якщо handlers/<type>.js відсутній або порожній, оновлення цього типу ігноруються, і платформа нічого для них не запускає. Тому залишайте лише ті обробники, які вам дійсно потрібні: кожен доданий вами обробник — це ще один тип оновлення, для якого бот прокидається, а відсутність решти означає, що Telegram не запускає ваш код для оновлень, які ви все одно відкинули б.

Щоб створити каркас нового обробника, запустіть npx tgcloud add handlers/<type> .

endpoints/ — серверна частина вашого міні-додатку. Кожен файл — це функція, яку міні-додаток викликає за іменем, тому endpoints/getProfile.js відповідає на POST /api/getProfile. Платформа перевіряє, що виклик походить від користувача Telegram всередині вашого міні-додатку, перш ніж ваш код запуститься, і передає вам дані цього користувача. endpoints/ має плоску структуру, як і handlers/. Див. Endpoints .

.tgcloud/ — локальний стан машини, повністю керований CLI, включаючи збережені облікові дані. Він ігнорується Git, і вам ніколи не слід читати чи писати в нього вручну — використовуйте команди CLI.

Система модулів

Під час виконання модуль бачить рівно дві речі: SDK платформи та інші модулі у вашому проєкті. Немає пакетів npm, немає файлової системи і немає мережі, крім як через fetch в SDK.

Імпортуйте власні модулі так, як ви б це робили в будь-якому проєкті JavaScript — відносним шляхом із розширенням .js — а SDK — за його назвою:

// tgcloud/handlers/message.js
import { users } from '../schema.js';              //  модуль схеми
import { addItem } from '../lib/cart.js';          //  модуль lib
import { format } from '../lib/internal/fmt.js';  //  вкладений модуль lib
import { db, api, fetch } from 'sdk';              //  SDK платформи
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        //  сусідній модуль
import { users } from '../schema.js';

Відносний імпорт повинен закінчуватися на .js і не може виходити за межі tgcloud/:

import { users } from '../schema';       //  додайте .js
import config from '../../config.js';    //  поза tgcloud/
import { z } from 'zod';                 //  жодних пакетів npm

Кожен модуль також має ім’я — свій шлях всередині tgcloud/ без розширення, наприклад schema, lib/cart або handlers/message. Це ім’я використовується npx tgcloud run і виводом CLI, і модуль також може бути імпортований за ним (from 'lib/cart', без розширення). Віддавайте перевагу відносним імпортам у своєму коді. Вони зрозумілі вашому редактору — перехід до визначення, автодоповнення, перейменування.

Під час виконання доступні рівно дві речі: sdk та його підмодулі (sdk/db, sdk/api, sdk/fetch) — вся поверхня платформи, див. SDK — та ваші власні модулі в tgcloud/. Це повний список. Якщо ваш код робить import чогось іншого, він не спрацює. Це обмеження забезпечує швидке завантаження модулів і безпеку виконання.

Handlers (Обробники)

Обробник — це модуль у handlers/, чий default export платформа викликає, коли приходить відповідне оновлення.

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Ви сказали: ${message.text ?? '(без тексту)'}`,
  });
}

Обробник отримує корисне навантаження оновлення як аргумент — платформа розгортає Update Telegram для вас. handlers/message.js отримує Message (тобто update.message); handlers/callback_query.js отримує CallbackQuery ; і так далі. Другий аргумент обробника — це об’єкт контексту виклику ctx. Він містить необроблене Update як ctx.update — звертайтеся до нього, коли потрібно щось за межами корисного навантаження, наприклад update_id.

Обробник може бути async (зазвичай так і є) і може повертати значення. Він взаємодіє з Bot API, базою даних і вихідним HTTP через SDK.

Вам не потрібне справжнє оновлення, щоб протестувати обробник. npx tgcloud run виконує його на платформі з корисним навантаженням, яке ви надаєте, і вашим поточним локальним кодом:

npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "привіт" }'

Аргумент — це корисне навантаження — той самий об’єкт, який отримує ваш обробник — у форматі JSON5. Щоб надати ctx обробника (його другий аргумент), додайте --ctx, наприклад --ctx '{ update: { update_id: 1 } }'. Це запускається на ваших локальних файлах, тому ви можете спробувати зміни перед розгортанням. Див. run .

Endpoints (Кінцеві точки)

Кінцева точка — це модуль у endpoints/, який викликає ваш міні-додаток — місце для всього, чого фронтенд не може зробити самостійно, як-от читання та запис бази даних, виклик Bot API або зв’язок зі сторонніми сервісами з використанням ваших секретів.

// 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('Профіль не знайдено', { userId: user.id });
  return profile;
}

Перший аргумент, input, — це об’єкт JSON, надісланий міні-додатком. Другий, ctx, містить ctx.initData — init data міні-додатку (user, chat, start_param, auth_date, …), вже перевірені платформою. Вони мають таку ж структуру, як Telegram.WebApp.initDataUnsafe на клієнті, але тут ви можете їм довіряти. Виклик без дійсних init data відхиляється з помилкою 401 до того, як ваш код запуститься, тому ctx.initData.user — це дійсно та людина, яка відкрила ваш міні-додаток. Кінцеві точки можна викликати тільки таким чином — з міні-додатку вашого бота користувачем Telegram — тому немає публічного API для захисту і логіну для створення.

Все, що повертає кінцева точка, надсилається назад як JSON. Щоб відхилити виклик, throw new EndpointError(опис, параметри?). Міні-додаток отримує опис та об’єкт parameters у незмінному вигляді. Якщо вам потрібно розгалужувати логіку на стороні фронтенду залежно від типу помилки, додайте код у parameters — { code: 'OUT_OF_STOCK' }. Будь-який інший виняток повідомляється як загальна внутрішня помилка, а реальне повідомлення потрапляє тільки у ваші логи, тому внутрішні деталі ніколи не виточуються до клієнта.

Виклик кінцевої точки з міні-додатку

Міні-додаток викликає кінцеву точку за іменем за допомогою Telegram.WebApp.Serverless.call з офіційного Mini Apps SDK , telegram-web-app.js — скрипта, який ваш міні-додаток уже містить. Він надсилає init data за вас і викликає зворотний виклик із помилкою або результатом кінцевої точки:

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — опис; err.parameters — те, що містив ваш EndpointError
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

Вхідні дані (input) є необов’язковими і мають бути звичайним об’єктом. Без них виклик виглядає як Serverless.call('getProfile', callback).

err.type дорівнює 'ENDPOINT_ERROR', коли ваша кінцева точка викликала EndpointError. Будь-яке інше значення означає, що платформа відхилила виклик або кінцева точка несподівано зазнала невдачі, наприклад 'UNAUTHORIZED', коли init data відсутні або недійсні.

Імена кінцевих точок схожі на ідентифікатори JavaScript — букви, цифри та _, не повинні починатися з цифри (getProfile, place_order). Створіть каркас за допомогою npx tgcloud add endpoints/<ім'я> і спробуйте його без міні-додатку за допомогою run , передаючи init data самостійно:

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

Що розгортається

Коли ви робите npx tgcloud push, CLI збирає кожен .js файл у schema.js, lib/, handlers/ та endpoints/ і надсилає цей набір як простір модулів вашого проєкту — разом із папкою збірки міні-додатку, якщо tgcloud.jsonc її вказує (див. нижче). Все, що є в хмарі, але відсутнє у вашому проєкті, видаляється, тому розгорнутий стан завжди відображає вашу папку — включаючи видалення. Нічого за межами tgcloud/ не розгортається як код — решта кореня проєкту залишається вашою. Всередині tgcloud/ .js файл, який не знаходиться в одному з цих місць (tgcloud/util.js, tgcloud/helpers/x.js), позначається, щоб він не залишився нерозгорнутим мовчки. Markdown, dot-файли та .tgcloud/ ніколи не розгортаються.

Фронтенд міні-додатку

Корінь проєкту — ваш, і це природне місце для фронтенду вашого міні-додатку — побудованого на React, Vue, Svelte або звичайному HTML, за допомогою Vite чи іншого збирача. Serverless розміщує його результати збірки поруч із вашим ботом. Один push розгортає код і статичні файли разом, і додаток обслуговується за адресою https://app<app_id>.tgcloud.ai/ (push друкує точну адресу — використовуйте її як URL міні-додатку в @BotFather ).

Повідомлення про зловживання. Міні-додатки на tgcloud.ai публікуються незалежними розробниками. Щоб повідомити про незаконний контент або зловживання на піддомені app<app_id>.tgcloud.ai, напишіть на [email protected] .

Для початку створіть фронтенд за допомогою будь-якого інструменту, а потім додайте зверху Serverless:

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

Творець проєктів ніколи не перезаписує ваші файли. Він додає папку tgcloud/ поруч із фронтендом, розпізнає папку збірки та додає скрипт npm run deploy, який збирає додаток і розгортає його разом із ботом.

Те, що розміщується, налаштовується у tgcloud.jsonc, конфігураційному файлі в корені проєкту:

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

Потрібен лише "source". Це папка збірки, відносно кореня проєкту. Інші ключі "static" налаштовують спосіб обслуговування (нижче). init створює tgcloud.jsonc за вас. Коли він розпізнає фронтенд, він також заповнює "source":

Фронтенд init встановлює "source" на
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular нічого — спочатку налаштуйте статичний експорт, потім встановіть "source" на папку самостійно

З будь-якою іншою налаштуванням встановіть "source" на папку збірки самостійно.

Рядок $schema надає вашому редактору автодоповнення та перевірку для кожного ключа. Файл є JSON із дозволеними коментарями та висячими комами. Якщо інший ваш інструмент повинен читати його як звичайний JSON, назвіть його tgcloud.json — тоді він має бути суворим JSON.

Збирайте проєкт перед push. push завантажує папку точно такою, якою вона є на диску — за винятком файлів та папок, імена яких починаються з крапки, вони пропускаються — і ніколи не запускає вашу збірку, тому звичайний npx tgcloud push після редагування фронтенду розгорне попередню збірку. Використовуйте npm run deploy (він запускає ваш скрипт build, а потім tgcloud push) або збирайте самостійно спочатку.

Варіанти обслуговування:

  • spa — подавати index.html для шляхів, які не існують і не мають розширення файлу, щоб клієнтський роутер працював при перезавантаженні. Шляхи з розширенням все одно отримують 404.
  • immutable — шаблони для хешованих файлів контенту (/assets/* у Vite), які браузери можуть кешувати протягом року. Використовуйте тільки для таких файлів. Будь-який інший файл, що підпадає під це, залишиться застарілим у браузерах на рік, і відкликати його неможливо.
  • headers — заголовки відповіді для шляхів, яким відповідає правило (точний шлях або префікс, що закінчується на /*). Значення null видаляє заголовок.
  • redirects — правила перенаправлення, де перемагає перше збіг. Існуючий файл завжди перемагає правило. Статус за замовчуванням — 301.
  • "static": "dist" — скорочення для { "source": "dist" }, коли вам більше нічого не потрібно.
  • "static": false — видалити розгорнутий сайт при наступному push. Видалення ключа просто припиняє керування статичними файлами, і те, що вже розгорнуто, залишається.

Сайт обслуговується з кореня свого хоста, тому базовий шлях (/) вашого збирача є правильним. Telegram Web відкриває міні-додатки в iframe, тому не додавайте X-Frame-Options або обмежувальну політику frame-ancestors.

Той самий хост відповідає на ваші кінцеві точки за шляхом /api/ — саме туди звертається Telegram.WebApp.Serverless.call('getProfile', …). Не розміщуйте власні файли за шляхом /api/ у папці збірки.

База даних

Кожен бот отримує власну базу даних — сховище на основі SQLite, яке зберігає стан між викликами і доступне кожному модулю через db. Ви описуєте свої таблиці в schema.js за допомогою невеликого типізованого DSL; ви читаєте та пишете їх за допомогою конструктора запитів; і ви розвиваєте їх за допомогою перевірених міграцій.

Знаєте Drizzle ORM ? Тоді ви вже знаєте, як спілкуватися з базою даних тут. DSL схеми та конструктор запитів наслідують Drizzle — конструктори колонок, select().from().where(), оператори, onConflictDoUpdate, .returning() та тег sql поводяться так, як ви очікуєте, тому читання та запис даних — це знайомий API, який ви вже використовуєте. Ви просто імпортуєте його з sdk/db, а кілька особливостей платформи (зокрема відсутність зовнішніх ключів ) вказані там, де вони виникають.

Оголошення таблиць

Таблиці — це іменовані експорти в schema.js. Виклик table() створює опис під час завантаження (він не торкається бази даних); платформа виявляє експортовані таблиці, коли ви розгортаєте schema.js, і мігрує базу даних у відповідність.

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 — ім’я таблиці SQL;
  • columns — карта JS-властивість → визначення колонки;
  • extras — необов’язковий зворотний виклик (t) => ({ … }), де t відкриває колонки (t.created — це посилання на цю колонку) — оголошуйте індекси та обмеження рівня таблиці тут.

Типи колонок

Фабрика Тип SQLite Примітки
text() TEXT
integer() INTEGER
real() REAL псевдонім float()
numeric() NUMERIC
blob() BLOB читає/пише Uint8Array
boolean() INTEGER зберігається як 0/1, читається як true/false
json() TEXT авто JSON.stringify / JSON.parse

Аргумент імені колонки необов’язковий — пропустіть його, і буде використано ключ JS. Опція mode контролює, як значення конвертуються між SQLite та JavaScript.

mode Зберігається як JS значення
boolean INTEGER 0/1 boolean
json TEXT (JSON) будь-який об’єкт/масив
timestamp INTEGER (unix секунди) Date
timestamp_ms INTEGER (unix мс) Date
bytes BLOB Uint8Array

boolean() та json() — це скорочення для integer(name, { mode: 'boolean' }) та text(name, { mode: 'json' }).

blob() читає та пише Uint8Array — середовище виконання не має Buffer з Node (а Buffer є підкласом Uint8Array, тому це портативний базовий тип). Опція mode вище регулює тільки те, як значення кодується, незалежно від типу зберігання колонки: тому blob('col', { mode: 'json' }) — це аналог json() для BLOB — те саме JSON-кодування, яке зберігається в колонці BLOB, а не TEXT.

TLDR: blob() використовує Uint8Array. mode контролює кодування, а не зберігання, тому blob(..., { mode: 'json' }) зберігає JSON як BLOB, тоді як json() зберігає його як TEXT.

Модифікатори колонок

Модифікатори колонок додаються в ланцюжок після колонки.

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' })  // або 'virtual'
text('email').deprecated('replaced by login')   // позначає колонку для видалення

.default() у колонці json() кодує значення за вас; дефолтне значення sql`…` передається як є. .deprecated() є термінальним — див. Міграції .

Індекси та обмеження

Індекси та обмеження оголошуються у зворотному виклику extras, де колонки доступні в області видимості.

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})`) ,          // індекс виразу
  active: index('idx_active').on(t.userId).where(sql`done = 0`) ,  // частковий індекс
}));

Модифікатори рівня таблиці йдуть після table(...): .strict(), .withoutRowid(), .deprecated('причина').

Відсутність зовнішніх ключів

Середовище виконання працює з вимкненою PRAGMA foreign_keys. Оголошений зовнішній ключ був би мовчки інертним — жодних каскадів, жодного захисту від сиріт — що гірше, ніж взагалі нічого, тому DSL робить це неможливим. Зокрема, .references() та foreignKey() рівня таблиці викликають помилку при оголошенні, і схема, яка їх використовує, не буде розгорнута.

Вам слід моделювати зв’язки за допомогою звичайних колонок (userId: integer('user_id')) і забезпечувати цілісність у коді вашої програми: вставляйте батьків перед дітьми, видаляйте дітей перед батьками, правильно обробляйте помилки та очищуйте сиріт за допомогою LEFT JOIN … WHERE parent.id IS NULL, коли це потрібно.

Відсутність зовнішніх ключів — це свідоме обмеження, а не недогляд. Враховуйте це заздалегідь при плануванні бота.

Запити

db — це конструктор запитів із вільним ланцюжком викликів. Кожен запит є асинхронним — термінальні методи (.all(), .get(), .values(), .run()) повертають Promises, тому завжди використовуйте 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();                          // всі рядки
await db.select().from(todos).where(eq(todos.id, 1)).get();   // перший рядок або null
await db.select().from(todos).values();                       // рядки як масиви значень

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

// користувацька проекція: { alias: columnRef | sqlExpr | aggregate }
await db.select({ id: todos.id, title: todos.text, n: count() })
  .from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();

// підрахунок рядків — помічник, не термінал конструктора:
await db.$count(todos);                        // всі рядки
await db.$count(todos, eq(todos.done, false)); // з фільтром
  • Ланцюгові методи: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Термінали: .all(), .get(), .values().
  • Підрахунок рядків: db.$count(table, where?) (або count() у проекції).

Insert, update, delete:

await db.insert(todos).values({ userId: 1, text: 'Купити молоко' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // пакет
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();

Зауважте, що пакетна вставка — це один запит, тому вона обмежена лімітом змінних SQLite (рядки × колонки); якщо ви вийдете за межі, вставка видасть помилку — розбивайте на частини самостійно.

Оператори імпортуються з 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) з кількома аргументами — це те саме, що and(a, b). Другий аргумент порівняння за замовчуванням є значенням, але може бути іншою колонкою або sql`…` — наприклад, eq(a.x, b.y). Агрегати (count/sum/avg/min/max) — це фрагменти sql для проекції .select({ … }).

Raw SQL — коли конструктора недостатньо, використовуйте сирий SQL. Режим обирається методом: db.run для записів, db.all для багатьох рядків, db.get для одного рядка, db.values для рядків як позиційних масивів.

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, 'купити молоко']]

Запис вирішується у результат виконання — rowsAffected, lastInsertRowid та rows, які містять clause RETURNING, якщо SQL його мав. Використовуйте db.values, коли запит проектує дві колонки з однаковим ім’ям (join типу SELECT u.id, o.id): db.all робить ключами рядків імена колонок, тому другий id перезаписав би перший.

Тег sql`…` перетворює ${value} на пов’язаний параметр, а ${table.column} — на ідентифікатор, і з’єднує вкладені фрагменти sql. Використовуйте sql.raw('…') для літерала без параметрів.

Raw-запити не прив’язані до таблиці, тому їхні рядки повертаються без конвертації режиму — boolean — це 0/1, JSON-колонка — це рядок, timestamp — це число. Тільки прив’язаний до таблиці конструктор конвертує значення.

Міграції

Ваша база даних змінюється в міру зростання бота. Платформа зберігає це в безпеці, розділяючи розгортання коду від зміни даних і класифікуючи кожну зміну схеми за ступенем ризику.

Deploy ніколи не торкається бази даних. Коли ви робите npx tgcloud push зі зміненим schema.js, платформа записує нову схему і повідомляє вам, що база даних змінила б — але нічого не застосовує:

npx tgcloud push       # розгортає schema.js; повідомляє про очікувані зміни БД
npx tgcloud migrate    # переглянути та застосувати їх

npx tgcloud migrate обчислює різницю між вашою схемою і живою базою даних і проводить вас крізь неї. Вас завжди запитають перед будь-яким застосуванням. Це означає, що рутинне розгортання коду ніколи не зможе випадково викликати міграцію даних.

Кожна очікувана зміна має статус, який визначає, як migrate з нею поводиться:

Статус Значення У migrate
safe Додаткова і неблокуюча — нова таблиця, колонка або індекс Застосовується разом за один крок, після підтвердження
warning Потенційно деструктивна або повільна — видалення чогось, або індекс на величезній таблиці Подається по одному, кожна підтверджується окремо
manual Не може бути виконано автоматично — наприклад, зміна типу колонки Показано з інструкціями; ви виконуєте вручну
undocumented Існує в базі даних, але не у вашій схемі Показано для обізнаності; не застосовується

Безпечні зміни швидкі та в дусі зворотні, тому вони проходять разом. Кожне попередження — це свідоме, індивідуальне підтвердження — немає “застосувати все” для деструктивних змін.

Ручні зміни йдуть із причиною та запропонованою дією. migrate завершується підсумком: скільки змін застосовано, пропущено, очікують ручного виправлення або відсутні у вашій схемі.

Див. migrate для прапорців (--dry-run, --safe, --yes, --local).

Видалення об’єктів

Видалення таблиці або колонки з schema.js не видаляє їх — це зробило б випадкове видалення катастрофічним. Щоб видалити щось, позначте це як застаріле (deprecated):

// видалити колонку
text('email').deprecated('замінено на login')

// видалити всю таблицю
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('не використовується');

При наступному migrate застарілий об’єкт з’являється як видалення зі статусом warning, яке ви підтверджуєте окремо. Як тільки воно зникне, видаліть оголошення.

Зміна типу колонки

Зміни типу — ручні — SQLite не завжди може зробити їх на місці, а приведення існуючих значень — це питання судження. migrate покаже зміну та її обґрунтування; виконайте її самостійно за допомогою raw SQL (db.run(...)), зазвичай шляхом створення нової колонки або таблиці, копіювання даних та заміни.

SDK

Під час виконання модуль має одну бібліотеку: sdk. Вона об’єднує три речі, необхідні бот-бекенду — базу даних, Telegram Bot API та вихідний HTTP — нічого не потрібно встановлювати і жодних облікових даних для налаштування. База даних (db) описана в База даних ; цей розділ охоплює api, fetch та глобальний об’єкт console.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // вся поверхня
// або з підмодулів:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Імпорт Що це таке
db База даних — конструктор запитів та схема DSL → База даних
api Telegram Bot API — api.sendMessage(...) → нижче
fetch Вихідний HTTP → нижче
InputFile Файл (байти + ім’я) для завантаження — працює з api та fetch → Файли
EndpointError Відхилити виклик міні-додатку з повідомленням, яке він може показати → Endpoints

Імпортуйте модулі вашого власного проєкту відносним шляхом із розширенням .js (from '../schema.js', from '../lib/cart.js'). Див. Система модулів .

Bot API

api дає вам увесь Telegram Bot API . Викликайте будь-який метод як api.<метод>(params). Кожен поточний — і майбутній — метод Bot API працює без оновлення SDK.

import { api } from 'sdk';

const me = await api.getMe();                            // → розгорнутий результат
await api.sendMessage({ chat_id: id, text: 'Привіт!' });
await api.editMessageText({ chat_id, message_id, text: 'Оновлено' });
await api.answerCallbackQuery({ callback_query_id, text: 'Готово' });

Конверт відповіді розгорнуто. Bot API зазвичай загортає результати в { ok: true, result: … }. api повертає result безпосередньо — getMe() вирішується в об’єкт користувача, а не в обгортку. Параметри використовують власні snake_case імена Bot API (chat_id, message_id, reply_markup, …).

Помилки викликають BotApiError. Коли Bot API повертає { ok: false }, виклик викликає BotApiError замість повернення хибного значення, тому ви не зможете випадково проігнорувати його. Помилка містить .code (код помилки Bot API), .description (людиночитане повідомлення), .method (який метод не вдався) та .parameters (додаткові дані, наприклад retry_after при 429, або migrate_to_chat_id). Спіймайте її, щоб обробити очікуваний збій, і повторно викличте решту:

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = повідомлення вже видалено; тут це нормально.
  } else {
    throw e;
  }
}

Файли

Працюйте з байтами файлів, а не просто file_id — завантажуйте нові файли та завантажуйте існуючі.

Завантаження — InputFile. Загорніть необроблені байти в InputFile (звичайний тримач: байти, ім’я файлу та необов’язковий MIME type) і передайте туди, де метод Bot API очікує файл:

import { api, InputFile } from 'sdk';

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

// файли всередині параметрів теж працюють — наприклад, альбом:
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' }) },
]});

Байти передаються в Telegram як справжнє multipart-завантаження — файл не буферизується повністю в один виклик до платформи — але ліміти Bot API Telegram все одно діють: бот зараз може завантажувати до 50 МБ на файл (і 10 МБ для фото).

Завантаження (отримання) — за file_id. Два помічники витягують байти файлу. Платформа вирішує file_id за вас (вона запускає getFile і завантажує вміст за лаштунками), тому ви ніколи не торкаєтеся токена бота або URL завантаження:

const bytes = await api.getFileContent(file_id);          // → Uint8Array (весь файл)

const { file, body } = await api.getFileStream(file_id);  // потокове отримання
file.file_unique_id;                                      // інформація getFile
for await (const chunk of body) { /* Uint8Array */ }      // або буферизуйте: await stream.bytes()

Bot API Telegram обмежує завантаження getFile до 20 МБ, тому це стеля для обох помічників.

HTTP

fetch — це клієнт, схожий на fetch, для виклику зовнішнього світу — сторонніх API, вебхуків, будь-чого через 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();

Відповідь відображає вебплатформу: res.status, res.statusText, res.ok (true для 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) та читачі тіла await res.json() / await res.text().

Ви також можете читати тіло поступово як потік — for await (const chunk of res.body) { … } — саме так ви споживаєте server-sent events або токен-по-токен вивід з AI API.

Помічники тіла встановлюють відповідний Content-Type за вас:

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

Тіла запитів також можуть містити файли та необроблені байти: передайте Uint8Array, InputFile або FormData (для multipart/form-data) як body, і байти будуть передані потоком — тому ви можете завантажувати на сторонні API так само, як відправляєте файли в 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', 'від мого бота');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });

В іншому він поводиться як стандартний fetch, який ви вже знаєте, з двома обмеженнями:

  • Відповідь читається як текст — await res.json() / await res.text() або потоком чанк-за-чанком через res.body.
  • Загальний обсяг відповіді обмежений 30 МБ. Це обмеження охоплює всю відповідь — потокове передавання через res.body дозволяє обробляти велике тіло поступово, але не підвищує ліміт.

Логування

Доступний стандартний глобальний console — нічого не потрібно імпортувати, він просто там, як і в будь-якому JavaScript. Його вивід фіксується та показується npx tgcloud run, що робить його вашим основним інструментом налагодження під час розробки.

console.log('обробка', { chatId: id });   // log / debug — звичайний
console.info('запущено');                     // info  — синій
console.warn('обмеження швидкості');                // warn  — жовтий
console.error(err);                          // error — червоний, з трасуванням стека

Кожен рядок позначений [файл:рядок], з якого він прийшов. console.error та console.trace додають повний стек, а console.warn — ні. Коли ви запускаєте npx tgcloud run для модуля, ці рядки друкуються з кольоровим префіксом рівня, часом від початку виконання та походженням — див. run .

Командний рядок (CLI)

tgcloud — це міст між папкою вашого проєкту та хмарою. Він створює каркаси проєктів, показує зміни, розгортає, запускає модулі та застосовує міграції бази даних. Потрібен Node.js 18 або новіше. Два способи його отримати:

# Рекомендовано — створити проєкт із встановленим CLI:
npm create @tgcloud/bot example_bot

# Або встановити CLI глобально та ініціалізувати порожню папку:
npm install -g @tgcloud/cli
tgcloud init

Пакет npm — @tgcloud/cli, а команда, яку він встановлює — tgcloud. Кожна команда працює з будь-якої підпапки вашого проєкту.

Команда Мета
init Створити каркас нового проєкту в поточній папці
add Створити каркас нового модуля (обробник, кінцева точка або lib)
login Прив’язати проєкт до бота (зберігає токен)
status Показати, що змінилося локально порівняно з хмарою
diff Показати зміни рядок за рядком
push Розгорнути змінені модулі та збірку міні-додатку в хмару
migrate Застосувати зміни схеми до бази даних
run Виконати модуль на платформі без розгортання
fetch Перевірити, що розгорнуто, без зміни локальних файлів
pull Привести локальні файли у відповідність до хмари
reset Відкинути локальні зміни; відновити зі стану хмари
webhook Перевірити та синхронізувати керований платформою вебхук
completion Вивести скрипт автодоповнення (bash/zsh/fish)
upgrade Перемістити структуру проєкту до-0.2.0 у tgcloud/

Автентифікація

Проєкт прив’язаний до одного бота за допомогою токена, який має форму app<id>:<secret>. Частина app<id> публічна і може виводитися; секретна частина ніколи не з’являється в логах або помилках.

Токен вирішується в такому порядку:

  1. TGCLOUD_TOKEN — змінна середовища — для CI; ніколи не записується на диск.
  2. .tgcloud/credentials — записується через npx tgcloud login .
  3. Нічого з цього — помилка, що вказує на npx tgcloud login.

CLI ніколи не запитує токен посеред команди. Вхід у систему — це завжди явний крок login. Якщо збережений токен стає недійсним, CLI видаляє його і просить увійти знову.

init

npx tgcloud init

Створює каркас нового проєкту в поточній директорії: папка tgcloud/ з schema.js, lib/, handlers/ та стартовим обробником, плюс tgcloud.jsonc, AGENTS.md, docs/ та папка стану .tgcloud/. Запустіть його в корені існуючого проєкту фронтенду — він додасть папку tgcloud/ і документацію, ніколи не перезаписуючи ваші файли. Якщо він розпізнає фронтенд, він також вкаже tgcloud.jsonc на папку збірки і додасть скрипт deploy (збірка, потім push) до вашого package.json — тільки якщо у вас ще немає скрипта deploy. Див. Фронтенд міні-додатку .

init відмовляється створювати проєкт всередині іншого проєкту. Повторний запуск у корені власного проєкту цілком нормальний і просто додає відсутні частини.

add

npx tgcloud add <ціль>

Створює каркас одного нового модуля, налаштованого і готового до редагування — обробник, кінцева точка або модуль lib/.

npx tgcloud add handlers/callback_query   # новий обробник оновлень
npx tgcloud add endpoints/getProfile      # нова кінцева точка для міні-додатку
npx tgcloud add lib/cart                  # новий спільний модуль

Зауважте, що add ніколи не перезаписує існуючий файл.

<ціль> — це шлях модуля (кінцеве .js необов’язкове). Для handlers/ ім’я має бути типом оновлення Telegram. Платформа рекламує дійсний набір, тому недійсне ім’я відхиляється відразу. handlers/ та endpoints/ є плоскими, тоді як lib/ може бути вкладеним (lib/payments/stripe).

Ім’я модуля обов’язкове. Надання лише каталогу — це помилка, але корисна: для handlers/ він перераховує типи оновлень, яких у вас ще немає, щоб ви могли скопіювати один із них.

$ npx tgcloud add handlers
Помилка: Вкажіть ім'я, наприклад "handlers/callback_query". Доступні типи handlers/: callback_query, inline_query, chat_member, …

Автодоповнення <Tab> пропонує той самий набір — див. completion .

Згенерований файл має діючий export default, тому обробник активний відразу після розгортання — нічого не потрібно розкоментовувати. Розгорніть новий модуль за допомогою push .

login

npx tgcloud login

Запитує ваш токен доступу до CLI — від @BotFather → ваш бот → Serverless → CLI Access → Access token — окремий токен від API-токена бота — перевіряє його на платформі та зберігає в .tgcloud/credentials.

login — це єдина команда, яка запитує токен, і вона вимагає інтерактивного терміналу. В CI встановіть TGCLOUD_TOKEN замість цього.

status

npx tgcloud status

Показує пофайлово, що змінилося між вашою робочою директорією та розгорнутою копією: змінено, нове, видалено, без змін. Працює офлайн, порівнюючи зі станом хмари на момент останнього push, pull або fetch. Також попереджає про зайві .js файли всередині tgcloud/, які не будуть розгорнуті.

Для проєкту з міні-додатком status також показує зміни в папці збірки з моменту останнього розгортання.

diff

npx tgcloud diff

Як status, але показує фактичні відмінності рядок за рядком для змінених модулів. Також офлайн.

push

npx tgcloud push [файли...]

Розгортає ваш проєкт у хмару одним атомарним пакетом.

Без аргументів він розгортає весь проєкт, і розгорнутий стан робиться точно дзеркальним вашій папці — модулі, які ви видалили локально, видаляються в хмарі.

З аргументами файлів або директорій (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), він розгортає лише зміни в цих файлах. Цільовий push ніколи не видаляє інші модулі. Ціль може бути вказана як шлях до файлу (tgcloud/handlers/message.js), шлях всередині tgcloud/ (handlers/message.js) або ім’я модуля (handlers/message) — всі вони означають один і той самий модуль.

Єдина опція — --force — щоб пропустити перевірку паралельності та перезаписати все, що є в хмарі. Використовуйте тільки тоді, коли впевнені (див. Підтримання синхронізації ).

Якщо tgcloud.jsonc вказує статичну папку, повний push розгортає її в тому ж пакеті — завантажуються лише змінені файли — і друкує адресу сайту. Збирайте проєкт спочатку, тому що push ніколи не запускає збірку (npm run deploy робить обидва кроки). npx tgcloud push dist/ розгортає тільки статичні файли, а push, що вказує лише модулі, залишає їх недоторканими.

Після розгортання, якщо schema.js змінився, а база даних не синхронізована, push друкує підсумок очікуваних змін і пропонує npx tgcloud migrate. Він ніколи не застосовує їх сам.

migrate

npx tgcloud migrate

Застосовує зміни схеми до бази даних. Обчислює різницю між schema.js і живою базою даних, а потім проводить вас крізь неї крок за кроком із лічильником [N/M]:

  • Короткий підсумок усіх очікуваних змін.
  • Безпечні зміни, застосовуються разом за один крок після вашого підтвердження.
  • Попередження (видалення, повільні операції), по одній за раз, кожна підтверджується окремо.
  • Ручні зміни, показані з причиною та запропонованою дією, не застосовуються автоматично.
  • Недокументовані об’єкти (у базі даних, але не у вашій схемі), показані для обізнаності.

Завершується підсумком: застосовано, пропущено, очікують ручного виправлення, відсутні у схемі.

Див. Міграції для моделі.

Опції: --dry-run (вивести все, нічого не застосовувати), --safe (автозастосувати безпечні зміни, пропустити попередження), --yes (автозастосувати безпечні зміни і кожне попередження, пропустити ручні — використовуйте обережно), --local (диференціювати з локальним schema.js замість розгорнутого). Без прапорця migrate вимагає терміналу і видає помилку в неінтерактивному середовищі замість вгадування.

run

npx tgcloud run <модуль> [аргументи] [--ctx <json5>]

Виконує обробник або кінцеву точку на платформі без розгортання, використовуючи ваші поточні локальні файли. Це швидка внутрішня петля для тестування логіки.

  • <модуль> — ім’я (пошук у handlers/ та endpoints/) або шлях типу handlers/message. Ім’я, яке існує в обох директоріях, повинно вказуватися з директорією.
  • [аргументи] — корисне навантаження, передане обробнику, у форматі JSON5 , тому можна не брати ключі в лапки. Це об’єкт типу оновлення, який отримує обробник (наприклад, Message для handlers/message), або input кінцевої точки, і це має бути об’єкт — не масив і не рядок.
  • --ctx <json5> — об’єкт контексту модуля (другий аргумент), також JSON5. Використовуйте його, щоб надати те, що ваш код читає з ctx — необроблене оновлення для обробника, --ctx '{ update: { update_id: 1 } }', або init data для кінцевої точки, --ctx '{ initData: { user: { id: 1 } } }'. run не перевіряє їх, оскільки ви тестуєте свою логіку, а не перевірки платформи.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "привіт" }'

Платформа запускає модуль проти простору модулів, зібраного з вашого локального проєкту (тому використовується і локально змінений код lib/), і повертає значення, усе виведене через console.* та час виконання. Читайте великі аргументи з файлу за допомогою npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Перевіряє, що зараз розгорнуто, не торкаючись ваших робочих файлів. Після цього status та diff порівнюють з останнім станом хмари. Корисно подивитися на конфлікт перед прийняттям рішення, як його вирішити.

pull

npx tgcloud pull

Приводить локальний проєкт у відповідність до хмари шляхом оновлення ваших робочих файлів до розгорнутого стану.

pull ніколи не пише папку збірки вашого міні-додатку.

reset

npx tgcloud reset

Відкидає локальні зміни та відновлює модулі з останнього відомого стану хмари. Використовуйте, щоб викинути експеримент. Папка збірки міні-додатку не чіпається.

webhook

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

Telegram доставляє оновлення вашому боту через вебхук, яким платформа керує за вас — ви ніколи не вказуєте його вручну. npx tgcloud webhook показує його поточний стан: URL, список allowed_updates, скільки оновлень очікує, остання помилка доставки (якщо є) і чи він синхронізований із вашими розгорнутими обробниками.

“Синхронізований” означає, що вебхук вказує на платформу, а його allowed_updates збігаються з розгорнутими обробниками — тому Telegram доставляє рівно ті типи оновлень, які ви обробляєте, і нічого іншого. Розгортання нового обробника (або видалення одного) може залишити вебхук поза синхронізацією до його оновлення; npx tgcloud status також позначає це.

npx tgcloud webhook sync виправляє це — він перевказує вебхук на платформу і перебудовує allowed_updates з ваших розгорнутих обробників. Додайте --drop-pending, щоб відкинути оновлення, які Telegram уже поставив у чергу перед синхронізацією (інакше вони будуть доставлені, як тільки вебхук знову стане здоровим).

completion

Примітка: tab-completion працює тільки тоді, коли чистий tgcloud є у вашому PATH — тому встановіть його глобально (npm install -g @tgcloud/cli) або іншим чином додайте бінарний файл у PATH. Він не може підключитися до npx.

tgcloud completion <bash|zsh|fish>

Друкує скрипт автодоповнення в stdout. Увімкніть його один раз, після чого <Tab> завершує команди, прапорці, папки модулів, типи оновлень обробників, яких у вас ще немає, і ваші локальні модулі.

# bash — потрібен пакет bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — переконайтеся, що `autoload -U compinit && compinit` запускається у вашому ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

Після цього перезапустіть оболонку (або перечитайте файл). Запуск tgcloud completion без оболонки знову друкує ці інструкції.

upgrade

npx tgcloud upgrade [--dry-run]

Одноразова міграція для проєктів, створених до CLI 0.2.0, які зберігали schema.js, lib/ та handlers/ у корені проєкту. Вона переміщує їх у tgcloud/. Ваша прив’язка до бота зберігається, нічого не потрібно перерозгортати. --dry-run друкує заплановані переміщення, нічого не торкаючись. Інші команди не працюють зі старою структурою, поки ви не зробите upgrade.

Підтримання синхронізації

Кожен проєкт має монотонно зростаючу ревізію в хмарі, яка збільшується при кожному розгортанні. CLI запам’ятовує ревізію, з якою він востаннє синхронізувався, і надсилає її при кожному push. Якщо хмара пішла вперед — тому що інша машина або колега розгорнули проєкт — push відхиляється замість мовчазного перезапису їхньої роботи, і CLI пропонує три шляхи вперед:

npx tgcloud fetch           # подивіться, що змінилося в хмарі, потім перевірте знову
npx tgcloud pull            # принесіть останнє у ваші робочі файли
npx tgcloud push --force    # перезаписати стан хмари (небезпечно)

Ця перевірка оптимістичної паралельності — причина, чому ви можете ділити бота в команді без процесу жорсткого розгортання. Команди завершуються з ненульовим кодом при збої — відхилене розгортання, помилка міграції, помилка автентифікації, модуль, який видав помилку під час run — тому вони чисто компонуються в скриптах та CI-пайплайнах.