Telegram Serverless memungkinkan Anda menjalankan kode backend untuk bot dan Mini App secara langsung pada infrastruktur Telegram — tanpa perlu mengelola server, tidak ada kontainer yang harus tetap aktif, dan tidak perlu memikirkan penskalaan. Anda cukup menulis modul JavaScript biasa, men-deploy-nya dengan satu perintah, dan Telegram akan menjalankannya dalam V8 sandbox yang cepat dan terisolasi yang berada tepat di samping Bot API dan database bawaan.

Jika Anda pernah menghubungkan bot ke VPS, fungsi cloud, atau panel hosting hanya untuk menjawab perintah /start, inilah bagian yang tidak perlu Anda lakukan lagi.

Mengapa serverless

Bot Telegram pada dasarnya adalah program yang bereaksi terhadap pembaruan (update). Secara tradisional, Anda harus meng-host program tersebut di suatu tempat yang selalu aktif, dapat diakses, dan aman — serta menjaganya tetap seperti itu. Telegram Serverless menghapus lapisan tersebut sepenuhnya:

  • Tanpa infrastruktur. Tidak ada mesin untuk disewa, ditambal (patch), atau dipantau. Kode Anda berjalan sesuai permintaan dan menskala secara otomatis mengikuti bot Anda.
  • Kredensial disertakan. Telegram Bot API, database berbasis SQLite, dan HTTP keluar tersedia untuk setiap modul secara langsung — tidak ada yang perlu diinstal, tidak ada kredensial untuk dihubungkan.
  • Eksekusi cepat dan terisolasi. Setiap pemanggilan berjalan dalam V8 isolate yang ringan, dekat dengan sistem Telegram sendiri, sehingga panggilan ke Bot API dan database Anda menjadi cepat dan andal.
  • Alur kerja pengembang yang nyata. Proyek hidup dalam folder di mesin Anda di bawah kendali versi (version control). Anda mengedit file, melihat dengan tepat apa yang berubah, men-deploy secara atomik, dan memperbarui skema database Anda dengan migrasi yang ditinjau — persis seperti cara Anda bekerja dengan hal lainnya.

Model mental: Cara kerja proyek

Anda bekerja di tiga tempat, dan ketiganya terhubung satu sama lain dengan rapi:

Tempat Apa yang ada di sana
Folder proyek Anda Modul JavaScript — skema, kode bersama, penangan pembaruan (update handler), endpoint Mini App — dan front-end Mini App jika ada
Cloud Salinan modul yang telah di-deploy, database bot Anda, dan file Mini App
tgcloud CLI Jembatan — alat ini menunjukkan perbedaan dan menyinkronkannya

Anda tidak perlu melakukan SSH ke apa pun. Anda mengedit file secara lokal, menjalankan npx tgcloud push, dan platform akan menangani sisanya. Lalu lintas bot Anda ditangani oleh modul yang telah di-deploy; database Anda tetap tersimpan di antara pemanggilan.

Sebuah proyek hanya memiliki empat jenis kode, semuanya di dalam satu folder tgcloud/:

tgcloud/
├─ endpoints/  # fungsi yang dipanggil oleh Mini App Anda
├─ handlers/   # fungsi yang bereaksi terhadap jenis pembaruan Telegram
├─ lib/        # kode bersama yang Anda impor dari mana saja
└─ schema.js   # tabel database Anda

Saat pembaruan tiba — pesan, penekanan tombol, inline query — Telegram merutekannya ke penangan (handler) yang cocok (handlers/message.js, handlers/callback_query.js, …) dan memanggil ekspor default-nya. Fungsi tersebut berkomunikasi dengan Bot API dan database melalui SDK, lalu selesai. Itulah seluruh loop-nya. Pembaruan tanpa penangan yang cocok akan diabaikan begitu saja, jadi Anda hanya perlu menambahkan penangan yang Anda butuhkan.

Mini App bersifat opsional. Jika bot Anda memilikinya, front-end-nya berada di proyek yang sama, di samping folder tgcloud/. Serverless akan meng-host output build-nya dan menjalankan endpoints/ yang dipanggilnya. Lihat Front-end Mini App .

Demo singkat

Berikut adalah demo bot yang lengkap dan berfungsi. Bot ini membalas setiap pesan dan mengingat berapa banyak pesan yang telah ia lihat dari setiap 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;

  // Masukkan counter, atau tambahkan jika chat ini sudah memilikinya — dan dapatkan
  // baris yang dihasilkan dalam pernyataan yang sama melalui .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: `Halo! Saya telah melihat ${row.seen} pesan dari Anda.`,
  });
}

Deploy bot Anda:

npx tgcloud push       # mengunggah modul
npx tgcloud migrate    # membuat tabel `counters`

Itu adalah bot langsung dengan status persisten tanpa server. Semuanya — api, db, DSL table() — dijelaskan di bagian bawah.

Serverless adalah backend umum untuk bot Telegram dan Mini App, bukan templat untuk satu jenis aplikasi saja. Ini sangat ideal untuk:

  • Bot AI Percakapan yang perlu menyimpan status per-pengguna di database.
  • Backend Mini App yang menyimpan data pengguna dan menyajikan konten dinamis.
  • Game dan Alat — termasuk papan peringkat, kuis, dan lainnya.
  • Otomasi dan Integrasi yang memanggil API HTTP pihak ketiga dan mengirimkan hasilnya ke chat.

Memulai

Panduan ini akan membawa Anda dari folder kosong hingga memiliki bot aktif yang menjawab pesan dan menyimpan data. Panduan ini mengasumsikan Anda telah menginstal Node.js versi 18 atau lebih baru dan memiliki bot yang terdaftar di @BotFather . Di akhir, Anda akan menggunakan setiap perintah yang Anda butuhkan sehari-hari: push, migrate, run, dan status.

Sebelum melakukan apa pun, aktifkan Serverless. Di @BotFather , buka bot Anda → Serverless dan aktifkan. Itu akan mengaktifkan fitur tersebut untuk bot ini dan membuka kunci token akses CLI, penangan, pustaka, dan databasenya.

1. Buat proyek

Cara tercepat untuk memulai adalah menggunakan pembuat proyek (project creator), yang akan menyusun struktur proyek dan menginstal CLI ke dalamnya:

npm create @tgcloud/bot contoh_bot
cd contoh_bot

Argumennya adalah folder target: masukkan . untuk menyusun di folder saat ini, atau jalur mana pun. Ini juga bekerja di folder yang sudah ada dan tidak akan pernah menimpa file yang sudah Anda miliki.

Ini memberi Anda proyek yang siap diedit:

contoh_bot/
├─ tgcloud/             # semua yang dijalankan oleh platform
│  ├─ handlers/
│  │  └─ message.js     # penangan pesan pemula (mengulang teks kembali)
│  ├─ lib/              # modul bersama Anda di sini (kosong saat awal)
│  └─ schema.js         # tabel database Anda
├─ docs/
│  └─ tgcloud-sdk.md    # referensi SDK (untuk Anda dan alat AI Anda)
├─ AGENTS.md            # orientasi untuk asisten pengkodean AI
├─ package.json
└─ tgcloud.jsonc        # konfigurasi proyek

File yang dihasilkan sudah terdokumentasi sendiri — masing-masing berisi contoh komentar tentang apa yang bisa Anda lakukan selanjutnya.

CLI diinstal ke dalam proyek sebagai dev-dependency lokal, jadi Anda menjalankannya dengan npx tgcloud <command> (npx menemukan salinannya di node_modules proyek Anda), atau melalui pintasan npm run yang ditambahkan oleh pembuat proyek ke package.json (npm run deploy, npm run status). Secara default tidak ada tgcloud global di PATH Anda.

Anda juga dapat menginstalnya secara global — npm install -g @tgcloud/cli — jika Anda lebih suka mengetik tgcloud langsung dari mana saja. Itu berguna untuk menjalankan tgcloud init di folder kosong mana pun. Apa pun caranya, Anda akan mendapatkan proyek yang sama.

Membangun Mini App? Mulailah dari front-end-nya terlebih dahulu. Buat dengan alat Anda yang biasa, lalu jalankan pembuat proyek di folder tersebut. Lihat Front-end Mini App .

2. Hubungkan bot Anda

Setiap proyek terikat pada satu bot. Hubungkan dengan login, yang akan meminta token akses CLI Anda ( @BotFather → bot Anda → Serverless → CLI Access → Access token — token terpisah dari bot API token Anda) dan menyimpannya secara lokal:

npx tgcloud login

Token tersebut memiliki format app<id>:<secret>. CLI menyimpannya di .tgcloud/, yang diabaikan oleh git (git-ignored), dan tidak akan pernah mencetak bagian secret-nya. Login adalah satu-satunya waktu Anda diminta untuk memberikannya — lihat Autentikasi untuk mengetahui bagaimana token diselesaikan di CI.

3. Lihat sekitar

Dua perintah memberi tahu Anda posisi proyek setiap saat, keduanya sepenuhnya offline:

npx tgcloud status     # apa yang berubah secara lokal vs salinan yang di-deploy
npx tgcloud diff       # perubahan baris demi baris

Tepat setelah init, semuanya baru dan belum ada yang di-deploy. status menunjukkan file pemula yang menunggu untuk diunggah.

4. Deploy

Kirim modul Anda ke cloud:

npx tgcloud push

push mengunggah setiap modul yang diubah dalam satu batch atomik dan memperbarui catatan lokal Anda tentang apa yang sekarang disimpan oleh cloud. Bot Anda sudah aktif: buka di Telegram dan kirim pesan — penangan pemula akan mengulang pesannya kembali.

Deploy tidak pernah menyentuh database Anda. Mengunggah kode dan mengubah skema database Anda adalah langkah yang sengaja dipisahkan, sehingga deploy kode tidak akan pernah mengejutkan Anda dengan migrasi data. Itulah tujuan langkah berikutnya.

5. Tambahkan tabel database

Mari buat bot agar mengingat sesuatu. Buka tgcloud/schema.js dan deklarasikan tabel:

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

Deploy skema tersebut, lalu terapkan ke database:

npx tgcloud push       # mengunggah skema baru
npx tgcloud migrate    # membuat tabel `messages`

push melaporkan bahwa skema tidak sinkron dan menunjukkan perubahan yang tertunda, tetapi tidak menerapkan apa pun. migrate memandu Anda melalui perubahan dan, setelah konfirmasi, membuat tabel tersebut. Model dua langkah ini — dan apa yang terjadi dengan perubahan yang lebih berisiko seperti menghapus tabel — dibahas dalam Migrasi .

6. Simpan dan baca data

Sekarang gunakan tabel dari penangan Anda. Edit tgcloud/handlers/message.js:

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

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

  // Hitung berapa banyak pesan yang telah kami simpan untuk chat ini.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Disimpan. Sejauh ini sudah ada ${count} pesan dari chat ini.`,
  });
}

Deploy penangan yang diperbarui dengan npx tgcloud push, lalu kirim beberapa pesan ke bot Anda dan lihat hitungannya bertambah. Database tetap ada di antara pemanggilan — itulah memori bot Anda.

7. Uji tanpa melakukan deploy

Anda tidak perlu melakukan deploy untuk mencoba perubahan. npx tgcloud run mengeksekusi penangan pada platform menggunakan file lokal Anda, tanpa memublikasikannya:

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

Argumennya adalah payload yang diterima penangan Anda — untuk handlers/message, sebuah Message — ditulis dalam JSON5 (sehingga Anda bisa melewati penanda kutip pada kunci). Perintah ini mencetak apa pun yang dicatat penangan dengan console.*, nilai balik, dan durasi eksekusinya. Ini adalah loop tercepat untuk mengulangi logika — tanpa deploy, tanpa menunggu pesan nyata.

8. Tetap sinkron

Saat Anda bekerja, beberapa perintah menjaga proyek lokal dan cloud Anda tetap selaras: npx tgcloud status menunjukkan apa yang berubah, npx tgcloud push men-deploy, npx tgcloud pull membawa proyek lokal Anda sejajar dengan cloud, npx tgcloud fetch memeriksa apa yang di-deploy tanpa menyentuh file Anda, dan npx tgcloud reset membuang perubahan lokal.

Jika dua orang (atau dua mesin) men-deploy ke bot yang sama, platform akan mendeteksi konflik dan push akan berhenti untuk membiarkan Anda pull terlebih dahulu — Anda tidak akan pernah bisa secara diam-diam menimpa pekerjaan orang lain. Lihat Tetap sinkron .

Membangun dengan AI

Lebih suka membangun dengan asisten AI — atau apakah satu-satunya pembuat kode di tim Anda adalah AI? Anda masih bisa mengirim bot. Kami telah mengambil langkah pertama untuk membuat agen AI merasa betah di dalam proyek: setiap proyek baru disusun dengan AGENTS.md dan referensi docs/tgcloud-sdk.md yang dibaca secara otomatis oleh alat pengkodean agen.

Bersama dengan runtime yang kecil dan mandiri — satu SDK, tidak perlu mengelola paket npm — hal ini memberi asisten awalan yang baik mengenai konvensi yang cenderung dilewatkan oleh pengkodean generik: impor relatif dengan .js, tidak ada kunci asing, setiap panggilan db bersifat asinkron, satu penangan per jenis pembaruan, dan alur push/migrate dua langkah.

Cobalah:

npm create @tgcloud/bot bot-saya
cd bot-saya
opencode            # atau Claude Code, Cursor, … — agen apa pun yang membaca AGENTS.md

Lalu mintalah dalam bahasa yang lugas:

Buat bot yang mengingat daftar tugas setiap orang — tambahkan item saat mereka mengirim teks, dan tampilkan seluruh daftar saat mereka mengirim /list.

Asisten akan mengedit schema.js dan penangan Anda untuk Anda; Anda meninjau, menguji perubahan secara instan dengan npx tgcloud run, lalu langsung men-deploy dengan npx tgcloud push dan npx tgcloud migrate. AGENTS.md adalah bagian dari proyek Anda — edit saat bot berkembang agar panduannya tetap akurat.

Saat bepergian dengan BotFather

Hanya membawa ponsel Anda? Seluruh proyek juga ada di @BotFather — buka bot Anda → Serverless dan Anda mendapatkan semua yang dikelola CLI, di layar sentuh:

  • Handlers — buat, edit, dan jalankan pengujian penangan pembaruan; BotFather menjaga webhook tetap sinkron dengan penangan yang Anda miliki (sama seperti In sync / Out of sync yang dilaporkan CLI).
  • Endpoints — fungsi yang dipanggil oleh Mini App Anda.
  • Library — modul lib/ bersama Anda.
  • Database — edit schema.js dalam sintaks serupa Drizzle, tinjau perubahan yang tertunda, dan terapkan; Save akan melakukan deploy.
  • File Mini App — front-end yang dibuka pengguna Anda di Telegram.
  • CLI Access — ambil token akses CLI di sini saat Anda kembali ke keyboard.

Ini adalah satu proyek cloud yang sama, jadi Anda dapat memulai penangan di ponsel Anda dan melakukan npx tgcloud pull ke laptop Anda nanti — tidak ada yang terikat pada satu klien saja. Menjalankan penangan bahkan menampilkan output Console langsung di chat, sama seperti npx tgcloud run .

Proyek dan modul

Proyek serverless adalah folder biasa di bawah kendali versi. Semua yang dijalankan platform berada dalam satu folder, tgcloud/; sisanya adalah milik Anda — front-end mini-app, konfigurasi, dokumen. Tidak ada langkah build untuk bot, tidak ada node_modules saat runtime, dan tidak ada titik masuk server.

Anatomi proyek

contoh_bot/
├─ tgcloud/             # bagian yang di-deploy — tidak ada yang lain
│  ├─ handlers/         # penangan pembaruan — flat, hanya satu level
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # fungsi yang dipanggil Mini App — flat, satu level
│  │  └─ getProfile.js
│  ├─ lib/              # modul bersama; subdirektori diperbolehkan
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # skema database — satu file
└─ .tgcloud/            # status CLI, git-ignored

Hanya file .js di dalam tgcloud/ — schema.js, lib/, handlers/, endpoints/ — yang di-deploy. Semuanya — Markdown, file konfigurasi, folder .tgcloud/, apa pun di luar tgcloud/ — tetap berada di mesin Anda. Nama modul bersifat relatif terhadap tgcloud/. File tgcloud/handlers/message.js adalah modul handlers/message, dan itulah nama yang digunakan npx tgcloud run.

Meningkatkan dari proyek lama? Proyek yang dibuat sebelum CLI 0.2.0 menyimpan schema.js, lib/ dan handlers/ di root proyek. Jalankan npx tgcloud upgrade sekali untuk memindahkannya ke tgcloud/.

schema.js — database Anda. Ia mendeklarasikan tabel sebagai ekspor bernama menggunakan skema DSL dan berada di tgcloud/ sebagai satu file. Ia di-deploy seperti modul lainnya, tetapi men-deploy-nya tidak pernah mengubah database — perubahan skema diterapkan secara terpisah dengan npx tgcloud migrate. Lihat Database .

lib/ — kode bersama, apa pun yang ingin Anda gunakan kembali di berbagai penangan: helper murni, lapisan akses database, pemformatan, integrasi dengan layanan luar. lib/ adalah satu-satunya direktori yang boleh berisi subdirektori (lib/internal/util.js, lib/payments/stripe.js), sehingga Anda dapat mengatur basis kode yang lebih besar sesuka Anda. Modul di lib/ tidak pernah dipanggil langsung oleh platform; mereka ada untuk diimpor oleh penangan dan satu sama lain.

handlers/ — titik masuk bot Anda. Setiap file sesuai dengan satu jenis pembaruan Telegram, dan platform merutekan setiap pembaruan masuk ke penangan yang cocok:

File Menangani
handlers/message.js Pesan masuk baru
handlers/inline_query.js Inline query masuk baru
handlers/callback_query.js Callback query masuk baru
… jenis pembaruan Bot API lainnya

handlers/ bersifat flat — tidak ada subdirektori. export default penangan adalah fungsi yang dipanggil platform (lihat Handlers ).

Jenis pembaruan ditangani hanya jika file penangannya ada dan tidak kosong. Jika tidak ada handlers/<type>.js — atau filenya kosong — pembaruan jenis tersebut diabaikan, dan platform tidak menjalankan apa pun untuk itu. Jadi simpanlah hanya penangan yang benar-benar Anda butuhkan: setiap penangan yang Anda tambahkan adalah jenis pembaruan lain yang dibangunkan bot Anda untuk diproses, dan membiarkan sisanya berarti Telegram tidak akan menjalankan kode Anda untuk pembaruan yang toh akan Anda abaikan.

Untuk menyusun penangan baru, jalankan npx tgcloud add handlers/<type> .

endpoints/ — sisi server dari Mini App Anda. Setiap file adalah fungsi yang dipanggil Mini App berdasarkan nama, jadi endpoints/getProfile.js menjawab POST /api/getProfile. Platform memeriksa apakah panggilan berasal dari pengguna Telegram di dalam Mini App Anda sebelum kode Anda berjalan, dan memberikan pengguna tersebut kepada Anda. endpoints/ bersifat flat, seperti handlers/. Lihat Endpoints .

.tgcloud/ — status lokal mesin yang dikelola sepenuhnya oleh CLI, termasuk kredensial Anda yang tersimpan. Folder ini diabaikan oleh git, dan Anda tidak boleh membaca atau menulis ke dalamnya secara manual — gunakan perintah CLI sebagai gantinya.

Sistem modul

Saat runtime, modul dapat melihat dua hal: SDK platform dan modul lain di proyek Anda. Tidak ada paket npm, tidak ada sistem file, dan tidak ada jaringan kecuali melalui fetch milik SDK.

Impor modul Anda sendiri seperti yang Anda lakukan di proyek JavaScript apa pun — melalui jalur relatif, dengan ekstensi .js — dan SDK berdasarkan namanya:

// tgcloud/handlers/message.js
import { users } from '../schema.js';              //  modul skema
import { addItem } from '../lib/cart.js';          //  modul lib
import { format } from '../lib/internal/fmt.js';  //  modul lib bersarang
import { db, api, fetch } from 'sdk';              //  SDK platform
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        //  tetangga
import { users } from '../schema.js';

Impor relatif harus diakhiri dengan .js dan tidak boleh keluar dari tgcloud/:

import { users } from '../schema';       //  tambahkan .js
import config from '../../config.js';    //  di luar tgcloud/
import { z } from 'zod';                 //  tidak ada paket npm

Setiap modul juga memiliki nama — jalurnya di dalam tgcloud/ tanpa ekstensi, seperti schema, lib/cart atau handlers/message. Nama itulah yang digunakan npx tgcloud run dan output CLI, dan modul juga dapat diimpor dengannya (from 'lib/cart', tanpa ekstensi). Lebih baik gunakan impor relatif dalam kode Anda. Editor Anda memahaminya — lompat ke definisi, lengkapi otomatis, ganti nama.

Apa yang tersedia saat runtime tepat dua hal: sdk dan submodulnya (sdk/db, sdk/api, sdk/fetch) — seluruh permukaan platform, lihat The SDK — dan modul Anda sendiri di tgcloud/. Itulah daftar lengkapnya. Jika kode Anda import yang lainnya, itu tidak akan terselesaikan. Batasan ini menjaga modul tetap cepat dimuat dan aman untuk dijalankan.

Handlers

Penangan (handler) adalah modul di handlers/ yang ekspor default-nya dipanggil oleh platform saat pembaruan yang cocok tiba.

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Anda berkata: ${message.text ?? '(tidak ada teks)'}`,
  });
}

Penangan menerima payload pembaruan sebagai argumennya — platform membuka Update Telegram untuk Anda. handlers/message.js mendapatkan Message (yaitu update.message); handlers/callback_query.js mendapatkan CallbackQuery ; dan seterusnya. Argumen kedua penangan adalah objek konteks per-pemanggilan, ctx. Ia membawa Update mentah sebagai ctx.update — gunakan saat Anda membutuhkan sesuatu di luar payload, seperti update_id.

Penangan bisa bersifat async (biasanya demikian) dan mungkin mengembalikan nilai. Ia mencapai Bot API, database, dan HTTP keluar melalui SDK.

Anda tidak perlu pembaruan nyata untuk menguji penangan. npx tgcloud run mengeksekusinya pada platform dengan payload yang Anda berikan dan kode lokal Anda saat ini:

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

Argumennya adalah payload — objek yang sama yang diterima penangan Anda — dalam JSON5. Untuk memberikan ctx penangan (argumen keduanya), tambahkan --ctx, misal --ctx '{ update: { update_id: 1 } }'. Ini berjalan terhadap file lokal Anda, jadi Anda bisa mencoba perubahan sebelum men-deploy-nya. Lihat run .

Endpoints

Endpoint adalah modul di endpoints/ yang dipanggil oleh Mini App Anda — tempat untuk apa pun yang tidak dapat dilakukan front-end sendiri, seperti membaca dan menulis database, memanggil Bot API, atau berbicara dengan layanan luar dengan rahasia Anda.

// 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('Profil tidak ditemukan', { userId: user.id });
  return profile;
}

Argumen pertama, input, adalah objek JSON yang dikirim Mini App. Argumen kedua, ctx, membawa ctx.initData — init data Mini App (user, chat, start_param, auth_date, …), yang sudah diverifikasi oleh platform. Ia memiliki bentuk yang sama seperti Telegram.WebApp.initDataUnsafe di klien, tetapi di sini Anda dapat memercayainya. Panggilan tanpa init data yang valid ditolak dengan 401 sebelum kode Anda berjalan, jadi ctx.initData.user benar-benar orang yang membuka Mini App Anda. Endpoint hanya dapat dipanggil dengan cara ini — dari Mini App bot Anda, oleh pengguna Telegram — sehingga tidak ada API publik yang perlu dilindungi dan tidak ada login yang perlu dibangun.

Apa pun yang dikembalikan endpoint akan dikirim kembali sebagai JSON. Untuk menolak panggilan, gunakan throw new EndpointError(description, parameters?). Mini App mendapatkan deskripsi dan objek parameters apa adanya. Jika Anda perlu bercabang pada jenis kesalahan di front-end, masukkan kode di parameters — { code: 'OUT_OF_STOCK' }. Pengecualian lain dilaporkan sebagai kesalahan internal generik, dan pesan aslinya hanya masuk ke log Anda, sehingga detail internal tidak pernah bocor ke klien.

Memanggil endpoint dari Mini App

Mini App memanggil endpoint berdasarkan nama dengan Telegram.WebApp.Serverless.call dari Mini Apps SDK resmi, telegram-web-app.js — skrip yang sudah disertakan Mini App Anda. Ia mengirimkan init data untuk Anda dan memberikan callback dengan kesalahan atau hasil endpoint:

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — deskripsinya; err.parameters — apa yang dibawa EndpointError Anda
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

Input bersifat opsional dan harus berupa objek biasa. Tanpanya, panggilannya menjadi Serverless.call('getProfile', callback).

err.type adalah 'ENDPOINT_ERROR' ketika endpoint Anda melemparkan EndpointError. Nilai lain berarti platform menolak panggilan atau endpoint gagal secara tak terduga, contohnya 'UNAUTHORIZED' ketika init data hilang atau tidak valid.

Nama endpoint seperti pengenal JavaScript — huruf, angka, dan _, tidak dimulai dengan angka (getProfile, place_order). Susun satu dengan npx tgcloud add endpoints/<name> , dan cobalah tanpa Mini App dengan run , berikan init data sendiri:

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

Apa yang di-deploy

Saat Anda melakukan npx tgcloud push, CLI mengumpulkan setiap file .js di bawah schema.js, lib/, handlers/, dan endpoints/, dan mengirimkan set tersebut sebagai ruang modul proyek Anda — bersama dengan folder build Mini App, jika tgcloud.jsonc menamainya (lihat di bawah). Apa pun yang ada di cloud tetapi tidak ada di proyek Anda akan dihapus, sehingga status yang di-deploy selalu mencerminkan folder Anda — termasuk penghapusan. Tidak ada apa pun di luar tgcloud/ yang di-deploy sebagai kode — sisa root proyek adalah milik Anda. Di dalam tgcloud/, file .js yang tidak ada di salah satu lokasi tersebut (tgcloud/util.js, tgcloud/helpers/x.js) akan ditandai, sehingga tidak secara diam-diam tidak ter-deploy. Markdown, dotfile, dan .tgcloud/ tidak pernah di-deploy.

Front-end Mini App

Root proyek adalah milik Anda, dan itu adalah tempat alami untuk front-end Mini App Anda — dibangun dengan React, Vue, Svelte atau HTML biasa, oleh Vite atau bundler lainnya. Serverless meng-host output build-nya di samping bot Anda. Satu push men-deploy kode dan file statis secara bersamaan, dan aplikasi disajikan di https://app<app_id>.tgcloud.ai/ (push mencetak alamat yang tepat — gunakan sebagai URL Mini App Anda di @BotFather ).

Melaporkan penyalahgunaan. Mini Apps di tgcloud.ai dipublikasikan oleh pengembang independen. Untuk melaporkan konten ilegal atau penyalahgunaan pada subdomain app<app_id>.tgcloud.ai, kirim email ke [email protected] .

Untuk memulai, buat front-end-nya terlebih dahulu, dengan alat apa pun yang Anda suka, lalu tambahkan Serverless di atasnya:

npm create vite@latest contoh_app   # atau create-vue, …
cd contoh_app
npm create @tgcloud/bot .

Pembuat proyek tidak pernah menimpa file Anda. Ia menambahkan folder tgcloud/ di samping front-end, mengenali folder build, dan menambahkan skrip npm run deploy yang membangun aplikasi dan men-deploy-nya bersama bot.

Apa yang di-host diatur dalam tgcloud.jsonc, file konfigurasi proyek di root proyek:

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

Hanya "source" yang diperlukan. Itu adalah folder build, relatif terhadap root proyek. Kunci lain dari "static" mengonfigurasi bagaimana ia disajikan (di bawah). init membuat tgcloud.jsonc untuk Anda. Saat mengenali front-end, ia juga mengisi "source":

Front-end init mengatur "source" ke
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular tidak ada — konfigurasi ekspor statis terlebih dahulu, lalu atur "source" ke foldernya sendiri

Dengan pengaturan lainnya, atur "source" ke folder build Anda sendiri.

Baris $schema memberikan editor Anda pelengkapan otomatis dan validasi untuk setiap kunci. File tersebut adalah JSON dengan komentar dan koma tambahan diizinkan. Jika alat Anda yang lain harus membacanya sebagai JSON biasa, beri nama tgcloud.json sebagai gantinya — maka harus berupa JSON yang ketat.

Build sebelum Anda melakukan push. push mengunggah folder persis seperti yang ada di disk — kecuali file dan folder yang namanya dimulai dengan titik, yang dilewati — dan tidak pernah menjalankan build Anda, jadi npx tgcloud push biasa setelah mengedit front-end akan men-deploy build sebelumnya. Gunakan npm run deploy (menjalankan skrip build Anda, lalu tgcloud push), atau bangun sendiri terlebih dahulu.

Opsi penyajian:

  • spa — sajikan index.html untuk jalur yang tidak ada dan tidak memiliki ekstensi file, sehingga router sisi klien berfungsi saat dimuat ulang. Jalur dengan ekstensi masih akan mendapatkan 404.
  • immutable — pola untuk file yang di-hash kontennya (/assets/* milik Vite) yang mungkin di-cache browser selama setahun. Gunakan hanya untuk file yang di-hash. File lain yang cocok di sini akan tetap usang di browser selama setahun, dan tidak ada cara untuk memanggilnya kembali.
  • headers — header respons untuk jalur yang cocok dengan aturan (jalur tepat, atau awalan yang diakhiri /*). Nilai null menghapus header.
  • redirects — aturan pengalihan, di mana kecocokan pertama menang. File yang ada selalu mengalahkan aturan. status adalah 301 kecuali Anda mengaturnya.
  • "static": "dist" — singkatan untuk { "source": "dist" } saat Anda tidak membutuhkan yang lain.
  • "static": false — hapus situs yang di-deploy pada push berikutnya. Menghapus kunci tersebut hanya menghentikan proyek ini dari mengelola file statis, dan apa pun yang di-deploy akan tetap ada.

Situs disajikan dari root host-nya sendiri, jadi base (/) default bundler Anda sudah benar. Telegram Web membuka Mini Apps dalam iframe, jadi jangan tambahkan X-Frame-Options atau kebijakan frame-ancestors yang restriktif.

Host yang sama menjawab endpoints Anda di bawah /api/ — di sanalah Telegram.WebApp.Serverless.call('getProfile', …) pergi. Jangan letakkan file Anda sendiri di bawah /api/ di folder build.

Database

Setiap bot mendapatkan databasenya sendiri — penyimpanan berbasis SQLite yang bertahan di antara pemanggilan dan tersedia untuk setiap modul melalui db. Anda mendeskripsikan tabel Anda di schema.js dengan DSL yang diketik; Anda membaca dan menulisnya dengan pembangun kueri yang lancar; dan Anda mengembangkannya dengan migrasi yang ditinjau.

Tahu Drizzle ORM ? Maka Anda sudah tahu cara berbicara dengan database di sini. Skema DSL dan pembangun kueri mengikuti Drizzle — pembangun kolom, select().from().where(), operator, onConflictDoUpdate, .returning(), dan tag sql semuanya berperilaku seperti yang Anda harapkan, jadi membaca dan menulis data adalah API familiar yang sudah Anda gunakan. Anda hanya mengimpornya dari sdk/db, dan beberapa spesifikasi platform (terutama tanpa kunci asing ) ditunjukkan di mana mereka muncul.

Mendeklarasikan tabel

Tabel adalah ekspor bernama di schema.js. Memanggil table() membangun deskripsi saat waktu muat (tidak menyentuh database); platform menemukan tabel yang diekspor saat Anda men-deploy schema.js dan memigrasi database agar sesuai.

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 adalah nama tabel SQL;
  • columns adalah peta properti JS → definisi kolom;
  • extras adalah callback opsional (t) => ({ … }) di mana t mengekspos kolom (t.created adalah referensi ke kolom itu) — deklarasikan indeks dan batasan tingkat tabel di sini.

Jenis kolom

Pabrik Tipe SQLite Catatan
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB membaca/menulis Uint8Array
boolean() INTEGER disimpan sebagai 0/1, dibaca sebagai true/false
json() TEXT auto JSON.stringify / JSON.parse

Argumen nama kolom bersifat opsional — hilangkan dan kunci JS akan digunakan. Opsi mode mengontrol bagaimana nilai dikonversi antara SQLite dan JavaScript.

mode Disimpan sebagai Nilai JS
boolean INTEGER 0/1 boolean
json TEXT (JSON) objek/array apa pun
timestamp INTEGER (detik unix) Date
timestamp_ms INTEGER (ms unix) Date
bytes BLOB Uint8Array

boolean() dan json() adalah singkatan untuk integer(name, { mode: 'boolean' }) dan text(name, { mode: 'json' }).

blob() membaca dan menulis Uint8Array — runtime tidak memiliki Buffer Node (dan Buffer adalah subkelas Uint8Array, jadi ini adalah tipe dasar yang portabel). mode di atas hanya mengatur bagaimana nilai dikodekan, terlepas dari jenis penyimpanan kolom: jadi blob('col', { mode: 'json' }) adalah rekan BLOB dari json() — pengkodean JSON yang sama, disimpan di kolom BLOB alih-alih TEXT.

TLDR: blob() menggunakan Uint8Array. mode mengontrol pengkodean, bukan penyimpanan, jadi blob(..., { mode: 'json' }) menyimpan JSON sebagai BLOB, sementara json() menyimpannya sebagai TEXT.

Pengubah kolom

Pengubah kolom dirantai ke kolom.

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' })  // atau 'virtual'
text('email').deprecated('digantikan oleh login')   // menandai kolom untuk dihapus

.default() pada kolom json() mengkodekan nilai untuk Anda; sql`…` default diteruskan apa adanya. .deprecated() adalah terminal — lihat Migrasi .

Indeks dan batasan

Indeks dan batasan dideklarasikan dalam callback extras, di mana kolom berada dalam cakupan.

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

Pengubah tingkat tabel dirantai setelah table(...): .strict(), .withoutRowid(), .deprecated('alasan').

Tanpa kunci asing (Foreign Keys)

Runtime berjalan dengan PRAGMA foreign_keys mati. Kunci asing yang dideklarasikan akan secara diam-diam tidak berpengaruh — tidak ada cascade, tidak ada perlindungan yatim — yang lebih buruk daripada tidak memiliki kunci asing sama sekali, jadi DSL membuatnya mustahil. Yaitu, .references() dan foreignKey() tingkat tabel melemparkan kesalahan saat dideklarasikan, dan skema yang menggunakannya tidak akan di-deploy.

Anda harus memodelkan hubungan dengan kolom biasa (userId: integer('user_id')) dan menegakkan integritas dalam kode aplikasi Anda: sisipkan induk sebelum anak, hapus anak sebelum induk, tangani kesalahan dengan benar, dan bersihkan data yatim dengan LEFT JOIN … WHERE parent.id IS NULL saat Anda membutuhkannya.

Absennya kunci asing adalah batasan yang disengaja, bukan pengawasan. Perhitungkan sejak awal saat merencanakan bot Anda.

Kueri

db adalah pembangun kueri yang lancar. Setiap kueri bersifat asinkron — metode terminal (.all(), .get(), .values(), .run()) mengembalikan Promise, jadi selalu 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();                          // semua baris
await db.select().from(todos).where(eq(todos.id, 1)).get();   // baris pertama atau null
await db.select().from(todos).values();                       // baris sebagai array nilai

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

// proyeksi kustom: { 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();

// jumlah baris — helper, bukan terminal pembangun:
await db.$count(todos);                        // semua baris
await db.$count(todos, eq(todos.done, false)); // dengan filter
  • Dapat dirantai: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Terminal: .all(), .get(), .values().
  • Hitung baris dengan db.$count(table, where?) (atau count() dalam proyeksi).

Sisipkan, perbarui, hapus:

await db.insert(todos).values({ userId: 1, text: 'Beli susu' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // batch
await db.insert(todos).values({ text: 'X' }).returning().run();        // RETURNING *

await db.insert(users).values({ tgId: 42, name: 'Ann' })
  .onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run();

await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();

Perhatikan bahwa sisipan batch adalah satu pernyataan, jadi dibatasi oleh batas variabel SQLite (baris × kolom); jika Anda melampauinya, sisipan akan error — bagi sendiri menjadi potongan.

Operator diimpor dari 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) dengan beberapa argumen sama dengan and(a, b). Argumen kedua perbandingan secara default adalah nilai, tetapi mungkin berupa kolom lain atau sql`…` — misal eq(a.x, b.y). Agregat (count/sum/avg/min/max) adalah fragmen sql untuk proyeksi .select({ … }).

SQL Mentah — saat pembangun tidak cukup, gunakan SQL mentah. Mode dipilih berdasarkan metode: db.run untuk penulisan, db.all untuk banyak baris, db.get untuk satu baris, db.values untuk baris sebagai array posisi.

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, 'beli susu']]

Penulisan diselesaikan menjadi hasil eksekusi — rowsAffected, lastInsertRowid, dan rows, yang membawa klausa RETURNING jika SQL memilikinya. Gunakan db.values saat kueri memproyeksikan dua kolom dengan nama yang sama (join seperti SELECT u.id, o.id): db.all memberi kunci baris berdasarkan nama kolom, jadi id kedua akan menimpa yang pertama.

Tag sql`…` mengubah ${value} menjadi parameter terikat dan ${table.column} menjadi pengenal, serta menyambungkan fragmen sql bersarang. Gunakan sql.raw('…') untuk literal tanpa parameter.

Kueri mentah tidak terikat pada tabel, jadi barisnya kembali tanpa konversi mode — boolean adalah 0/1, kolom JSON adalah string, timestamp adalah angka. Hanya pembangun terikat tabel yang mengonversi nilai.

Migrasi

Database Anda berubah seiring berkembangnya bot Anda. Platform menjaga keamanan hal tersebut dengan memisahkan deploy kode dari perubahan data, dan dengan mengklasifikasikan setiap perubahan skema berdasarkan risikonya.

Deploy tidak pernah menyentuh database. Saat Anda npx tgcloud push schema.js yang diubah, platform merekam skema baru dan memberi tahu Anda perubahan apa yang akan terjadi pada database — tetapi tidak menerapkan apa pun:

npx tgcloud push       # deploy schema.js; melaporkan perubahan DB yang tertunda
npx tgcloud migrate    # tinjau dan terapkan

npx tgcloud migrate menghitung perbedaan antara skema Anda dan database langsung serta memandu Anda melaluinya. Anda selalu diminta sebelum apa pun diterapkan. Ini berarti deploy kode rutin tidak akan pernah memicu migrasi data secara tidak sengaja.

Setiap perubahan tertunda membawa status yang menentukan bagaimana migrate memperlakukannya:

Status Makna Dalam migrate
safe Aditif dan tidak memblokir — tabel, kolom, atau indeks baru Diterapkan bersama dalam satu langkah, setelah konfirmasi
warning Berpotensi destruktif atau lambat — menghapus sesuatu, atau indeks pada tabel besar Ditampilkan satu per satu, masing-masing dikonfirmasi
manual Tidak dapat dilakukan secara otomatis — misal mengubah tipe kolom Ditampilkan dengan panduan; Anda melakukannya secara manual
undocumented Ada di database tetapi tidak di skema Anda Ditampilkan untuk kesadaran; tidak diterapkan

Perubahan aman cepat dan dapat dibalikkan secara prinsip, sehingga diterapkan bersama-sama. Setiap peringatan adalah konfirmasi individu yang disengaja — tidak ada “terapkan semua” untuk perubahan destruktif.

Perubahan manual datang dengan alasan dan tindakan yang disarankan. migrate berakhir dengan ringkasan: berapa banyak perubahan yang diterapkan, dilewati, menunggu perbaikan manual, atau tidak ada di skema Anda.

Lihat migrate untuk flag (--dry-run, --safe, --yes, --local).

Menghapus sesuatu

Menghapus tabel atau kolom dari schema.js tidak menjatuhkannya (drop) — itu akan membuat penghapusan tidak sengaja menjadi bencana. Untuk menghapus sesuatu, tandai sebagai usang (deprecated):

// hapus kolom
text('email').deprecated('digantikan oleh login')

// hapus seluruh tabel
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('tidak digunakan');

Pada migrate berikutnya, objek yang usang muncul sebagai penghapusan status peringatan yang Anda konfirmasi secara individual. Setelah hilang, hapus deklarasinya.

Mengubah tipe kolom

Perubahan tipe bersifat manual — SQLite tidak selalu dapat melakukannya di tempat, dan memaksa nilai yang ada adalah keputusan penilaian. migrate akan menunjukkan perubahan dan alasannya; lakukan sendiri dengan SQL mentah (db.run(...)), biasanya dengan membuat kolom atau tabel baru, menyalin data, dan menukarnya.

SDK

Saat runtime, modul memiliki satu pustaka: sdk. Ia menggabungkan tiga hal yang dibutuhkan backend bot — database, Telegram Bot API, dan HTTP keluar — tanpa perlu diinstal dan tidak ada kredensial untuk dikonfigurasi. Database (db) dibahas di Database ; bagian ini membahas api, fetch, dan console global.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // seluruh permukaan
// atau dari submodul:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Impor Apa itu
db Database — pembangun kueri dan skema DSL → Database
api Telegram Bot API — api.sendMessage(...) → di bawah
fetch HTTP Keluar → di bawah
InputFile File (byte + nama) untuk diunggah — bekerja dengan api dan fetch → File
EndpointError Tolak panggilan Mini App dengan pesan yang bisa ditampilkan → Endpoints

Impor modul proyek Anda sendiri melalui jalur relatif dengan ekstensi .js (from '../schema.js', from '../lib/cart.js'). Lihat Sistem modul .

Bot API

api memberi Anda seluruh Telegram Bot API . Panggil metode apa pun sebagai api.<method>(params). Setiap metode Bot API saat ini — dan masa depan — berfungsi tanpa pembaruan SDK diperlukan.

import { api } from 'sdk';

const me = await api.getMe();                            // → hasil yang tidak dibungkus
await api.sendMessage({ chat_id: id, text: 'Halo!' });
await api.editMessageText({ chat_id, message_id, text: 'Diperbarui' });
await api.answerCallbackQuery({ callback_query_id, text: 'Selesai' });

Amplop respons tidak dibungkus. Bot API biasanya membungkus hasil dalam { ok: true, result: … }. api mengembalikan result secara langsung — getMe() diselesaikan menjadi objek pengguna, bukan pembungkus. Parameter menggunakan nama snake_case Bot API sendiri (chat_id, message_id, reply_markup, …).

Kegagalan melemparkan BotApiError. Saat Bot API mengembalikan { ok: false }, panggilan melemparkan BotApiError alih-alih mengembalikan nilai falsy, sehingga Anda tidak bisa secara tidak sengaja mengabaikannya. Kesalahan membawa .code (kode kesalahan Bot API), .description (pesan yang dapat dibaca manusia), .method (metode mana yang gagal), dan .parameters (data tambahan seperti retry_after pada 429, atau migrate_to_chat_id). Tangkap untuk menangani kegagalan yang diharapkan dan lempar ulang sisanya:

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = pesan sudah hilang; tidak masalah di sini.
  } else {
    throw e;
  }
}

File

Bekerjalah dengan byte file, bukan hanya file_id — unggah file baru dan unduh yang sudah ada.

Unggah — InputFile. Bungkus byte mentah dalam InputFile (pemegang biasa: byte, nama file, dan MIME type opsional) dan berikan ke mana pun metode Bot API mengharapkan file:

import { api, InputFile } from 'sdk';

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

// file yang bersarang di dalam parameter juga berfungsi — misal album:
await api.sendMediaGroup({ chat_id, media: [
  { type: 'photo', media: new InputFile(a, 'a.jpg', { type: 'image/jpeg' }) },
  { type: 'photo', media: new InputFile(b, 'b.jpg', { type: 'image/jpeg' }) },
]});

Byte dialirkan ke Telegram sebagai unggahan multipart nyata — file tidak di-buffer seluruhnya ke dalam satu panggilan ke platform — tetapi batas Bot API Telegram masih berlaku: bot saat ini dapat mengunggah hingga 50 MB per file (dan 10 MB untuk foto).

Unduh — berdasarkan file_id. Dua helper menarik byte file kembali. Platform menyelesaikan file_id untuk Anda (ia menjalankan getFile dan mengambil konten di balik layar), jadi Anda tidak pernah menyentuh token bot atau URL unduhan:

const bytes = await api.getFileContent(file_id);          // → Uint8Array (seluruh file)

const { file, body } = await api.getFileStream(file_id);  // alirkan sebagai gantinya
file.file_unique_id;                                      // info getFile
for await (const chunk of body) { /* Uint8Array */ }      // atau buffer: await stream.bytes()

Bot API Telegram membatasi unduhan getFile hingga 20 MB, jadi itu adalah batas untuk kedua helper.

HTTP

fetch adalah klien mirip fetch untuk memanggil dunia luar — API pihak ketiga, webhook, apa pun melalui 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();

Respons mencerminkan platform web: res.status, res.statusText, res.ok (true untuk 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()), dan pembaca isi await res.json() / await res.text().

Anda juga dapat membaca isi secara bertahap sebagai stream — for await (const chunk of res.body) { … } — yang merupakan cara Anda mengonsumsi event yang dikirim server atau output token-demi-token dari API AI.

Helper isi mengatur Content-Type yang cocok untuk Anda:

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

Isi permintaan juga dapat membawa file dan byte mentah: berikan Uint8Array, InputFile, atau FormData (untuk multipart/form-data) sebagai body, dan byte akan dialirkan keluar — jadi Anda dapat mengunggah ke API pihak ketiga dengan cara yang sama seperti Anda mengirim file ke Telegram:

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

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

Selebihnya berperilaku seperti fetch standar yang sudah Anda kenal, dengan dua batasan:

  • Respons dibaca sebagai teks — await res.json() / await res.text(), atau dialirkan potongan demi potongan dengan res.body.
  • Total respons dibatasi hingga 30 MB. Batas itu mencakup seluruh respons — streaming dengan res.body memungkinkan Anda memproses isi besar secara bertahap, tetapi itu tidak menaikkan batasnya.

Logging

console global standar tersedia — tidak ada yang perlu diimpor, tersedia seperti di JavaScript mana pun. Outputnya ditangkap dan ditampilkan oleh npx tgcloud run, yang menjadikannya alat debug utama Anda selama pengembangan.

console.log('memproses', { chatId: id });   // log / debug — polos
console.info('dimulai');                     // info  — biru
console.warn('batas rate');                // warn  — kuning
console.error(err);                          // error — merah, dengan pelacakan tumpukan

Setiap baris ditandai dengan [file:line] asalnya. console.error dan console.trace menambahkan tumpukan lengkap, sementara console.warn tidak. Saat Anda npx tgcloud run modul, baris-baris ini dicetak dengan awalan berwarna per level, waktu sejak dijalankan, dan asalnya — lihat run .

Antarmuka baris perintah (CLI)

tgcloud adalah jembatan antara folder proyek Anda dan cloud. Alat ini menyusun proyek, menunjukkan apa yang berubah, men-deploy, menjalankan modul, dan menerapkan migrasi database. Alat ini membutuhkan Node.js 18 atau lebih baru. Dua cara untuk mendapatkannya:

# Direkomendasikan — buat proyek dengan CLI diinstal ke dalamnya:
npm create @tgcloud/bot contoh_bot

# Atau instal CLI secara global dan inisialisasi folder kosong:
npm install -g @tgcloud/cli
tgcloud init

Paket npm adalah @tgcloud/cli, dan perintah yang diinstalnya adalah tgcloud. Setiap perintah bekerja dari subfolder mana pun di proyek Anda.

Perintah Tujuan
init Menyusun proyek baru di folder saat ini
add Menyusun modul baru (penangan, endpoint, atau modul lib)
login Hubungkan proyek ke bot (menyimpan token)
status Tunjukkan apa yang berubah secara lokal vs cloud
diff Tunjukkan perubahan baris demi baris
push Deploy modul yang diubah dan build Mini App ke cloud
migrate Terapkan perubahan skema ke database
run Jalankan modul di platform tanpa melakukan deploy
fetch Periksa apa yang di-deploy, tanpa mengubah file lokal
pull Samakan file lokal dengan cloud
reset Buang perubahan lokal; pulihkan dari status cloud
webhook Periksa dan sinkronkan ulang webhook yang dikelola platform
completion Cetak skrip penyelesaian shell (bash/zsh/fish)
upgrade Pindahkan tata letak proyek pra-0.2.0 ke tgcloud/

Autentikasi

Proyek terikat pada satu bot melalui tokennya, yang memiliki bentuk app<id>:<secret>. Bagian app<id> bersifat publik dan boleh dicetak; secret tidak pernah muncul di log atau kesalahan.

Token diselesaikan dalam urutan ini:

  1. TGCLOUD_TOKEN variabel lingkungan — untuk CI; tidak pernah ditulis ke disk.
  2. .tgcloud/credentials — ditulis oleh npx tgcloud login .
  3. Tidak ada — kesalahan yang mengarahkan Anda ke npx tgcloud login.

CLI tidak pernah meminta token di tengah perintah. Login selalu merupakan langkah login eksplisit. Jika token yang disimpan menjadi tidak valid, CLI menghapusnya dan meminta Anda untuk login lagi.

init

npx tgcloud init

Menyusun proyek baru di direktori saat ini: folder tgcloud/ dengan schema.js, lib/, handlers/ dan penangan pemula, ditambah tgcloud.jsonc, AGENTS.md, docs/, dan folder status .tgcloud/. Jalankan juga di root proyek front-end yang sudah ada — ia akan menambahkan folder tgcloud/ dan dokumen, serta tidak pernah menimpa file Anda. Jika mengenali front-end, ia juga mengarahkan tgcloud.jsonc ke folder build dan menambahkan skrip deploy (build, lalu push) ke package.json Anda — hanya jika Anda belum memiliki skrip deploy. Lihat Front-end Mini App .

init menolak untuk membuat proyek di dalam proyek lain. Menjalankan kembali di root proyek sendiri tidak masalah dan hanya akan menambahkan apa yang hilang.

add

npx tgcloud add <target>

Menyusun satu modul baru, terhubung dan siap diedit — penangan, endpoint, atau modul lib/.

npx tgcloud add handlers/callback_query   # penangan pembaruan baru
npx tgcloud add endpoints/getProfile      # endpoint baru untuk Mini App
npx tgcloud add lib/cart                  # modul bersama baru

Perhatikan bahwa add tidak pernah menimpa file yang sudah ada.

<target> adalah jalur modul (ekstensi .js bersifat opsional). Untuk handlers/, namanya harus berupa jenis pembaruan Telegram. Platform mengiklankan set yang valid, sehingga nama yang tidak valid akan ditolak di depan. handlers/ dan endpoints/ bersifat flat, sedangkan lib/ bisa bersarang (lib/payments/stripe).

Nama modul diperlukan. Memberikan hanya direktori adalah kesalahan — tetapi kesalahan yang membantu: untuk handlers/ ia mencantumkan jenis pembaruan yang belum Anda miliki, sehingga Anda dapat menyalin salah satunya.

$ npx tgcloud add handlers
Kesalahan: Tentukan nama, misal "handlers/callback_query". Jenis handlers/ yang tersedia: callback_query, inline_query, chat_member, …

Pelengkapan <Tab> menawarkan set yang sama — lihat completion .

File yang dihasilkan memiliki export default aktif, jadi penangan langsung aktif segera setelah Anda melakukan deploy — tidak ada yang perlu di-uncomment. Deploy modul baru dengan push .

login

npx tgcloud login

Meminta token akses CLI Anda — dari @BotFather → bot Anda → Serverless → CLI Access → Access token, token terpisah dari bot API token Anda — memvalidasinya terhadap platform, dan menyimpannya ke .tgcloud/credentials.

login adalah satu-satunya perintah yang meminta token, dan membutuhkan terminal interaktif. Di CI, atur TGCLOUD_TOKEN sebagai gantinya.

status

npx tgcloud status

Menampilkan, per file, apa yang berubah antara direktori kerja Anda dan salinan yang di-deploy: dimodifikasi, baru, dihapus, tidak berubah. Perintah ini bekerja offline, membandingkan dengan status cloud terakhir Anda saat push, pull, atau fetch. Ia juga memperingatkan tentang file .js liar di dalam tgcloud/ yang tidak akan di-deploy.

Untuk proyek dengan Mini App, status juga menampilkan apa yang berubah di folder build sejak deploy terakhir.

diff

npx tgcloud diff

Seperti status, tetapi menampilkan perbedaan baris demi baris yang sebenarnya untuk modul yang diubah. Juga offline.

push

npx tgcloud push [files...]

Men-deploy proyek Anda ke cloud dalam satu batch atomik.

Dengan tanpa argumen, ia men-deploy seluruh proyek, dan status yang di-deploy dibuat agar persis mencerminkan folder Anda — modul yang Anda hapus secara lokal akan dihapus di cloud.

Dengan argumen file atau direktori (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), ia hanya men-deploy perubahan pada file tersebut. Push yang ditargetkan tidak pernah menghapus modul lain. Target dapat ditulis sebagai jalur file (tgcloud/handlers/message.js), jalur di dalam tgcloud/ (handlers/message.js), atau nama modul (handlers/message) — semuanya berarti modul yang sama.

Satu opsinya adalah --force — untuk melewati pemeriksaan konkurensi dan menimpa apa pun yang ada di cloud. Gunakan hanya saat Anda yakin (lihat Tetap sinkron ).

Jika tgcloud.jsonc menamai folder statis, push penuh men-deploy-nya dalam batch yang sama — hanya file yang berubah yang diunggah — dan mencetak alamat situs. Build terlebih dahulu, karena push tidak pernah menjalankan build Anda (npm run deploy melakukan keduanya). npx tgcloud push dist/ men-deploy file statis saja, dan push yang hanya menamai modul akan membiarkannya tidak tersentuh.

Setelah deploy, jika schema.js berubah dan database tidak sinkron, push mencetak ringkasan perubahan tertunda dan menyarankan npx tgcloud migrate. Ia tidak pernah menerapkannya sendiri.

migrate

npx tgcloud migrate

Menerapkan perubahan skema Anda ke database. Ia menghitung perbedaan antara schema.js dan database langsung, lalu memandu Anda melaluinya langkah demi langkah dengan penghitung [N/M] yang berjalan:

  • Ringkasan singkat semua perubahan tertunda.
  • Perubahan aman, diterapkan bersama dalam satu langkah setelah konfirmasi Anda.
  • Peringatan (penghapusan, operasi lambat), satu per satu, masing-masing dikonfirmasi secara terpisah.
  • Perubahan manual, ditampilkan dengan alasan dan tindakan yang disarankan, tidak diterapkan secara otomatis.
  • Objek yang tidak terdokumentasi (di database tetapi tidak di skema Anda), ditampilkan untuk kesadaran.

Ini berakhir dengan ringkasan: diterapkan, dilewati, menunggu perbaikan manual, tidak ada di skema.

Lihat Migrasi untuk modelnya.

Opsi: --dry-run (cetak semuanya, jangan terapkan apa pun), --safe (terapkan otomatis perubahan aman, lewati peringatan), --yes (terapkan otomatis perubahan aman dan setiap peringatan, lewati manual — gunakan dengan hati-hati), --local (bandingkan dengan schema.js lokal Anda alih-alih yang di-deploy). Tanpa flag, migrate memerlukan terminal dan error di lingkungan non-interaktif alih-alih menebak-nebak.

run

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

Mengeksekusi penangan atau endpoint pada platform tanpa deploy, menggunakan file lokal Anda saat ini. Ini adalah loop dalam yang cepat untuk menguji logika.

  • <module> — nama polos (dicari di bawah handlers/ dan endpoints/) atau jalur seperti handlers/message. Nama polos yang ada di kedua direktori harus ditulis dengan direktorinya.
  • [args] — payload yang diberikan ke penangan, ditulis dalam JSON5 sehingga Anda bisa melewati tanda kutip kunci. Ini adalah objek jenis pembaruan yang diterima penangan Anda (misal Message untuk handlers/message), atau input endpoint, dan harus berupa objek — bukan array atau string polos.
  • --ctx <json5> — objek konteks modul (argumen keduanya), juga JSON5. Gunakan untuk memberikan apa yang dibaca kode Anda dari ctx — pembaruan mentah untuk penangan, --ctx '{ update: { update_id: 1 } }', atau init data untuk endpoint, --ctx '{ initData: { user: { id: 1 } } }'. run tidak memverifikasinya, karena Anda menguji logika Anda, bukan pemeriksaan platform.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'

Platform menjalankan modul terhadap ruang modul yang dirakit dari proyek lokal Anda (jadi kode lib/ yang diubah secara lokal juga digunakan) dan mengembalikan nilai balik, apa pun yang dicatat dengan console.*, dan waktu yang berlalu. Baca argumen besar dari file dengan npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Memeriksa apa yang saat ini di-deploy, tanpa menyentuh file kerja Anda. Setelahnya, status dan diff membandingkan dengan status cloud terbaru. Berguna untuk melihat konflik sebelum memutuskan cara menyelesaikannya.

pull

npx tgcloud pull

Menyamakan proyek lokal Anda dengan cloud dengan memperbarui file kerja Anda ke status yang di-deploy.

pull tidak pernah menulis folder build Mini App Anda.

reset

npx tgcloud reset

Membuang perubahan lokal Anda dan memulihkan modul dari status cloud terakhir yang diketahui. Gunakan untuk membuang eksperimen. Folder build Mini App tidak disentuh.

webhook

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

Telegram mengirimkan pembaruan ke bot Anda melalui webhook, yang dikelola platform untuk Anda — Anda tidak pernah mengarahkannya ke mana pun secara manual. npx tgcloud webhook menampilkan statusnya saat ini: URL, daftar allowed_updates, berapa banyak pembaruan yang tertunda, kesalahan pengiriman terakhir (jika ada), dan apakah sudah sinkron dengan penangan yang Anda deploy.

“Sinkron” berarti webhook mengarah ke platform dan allowed_updates-nya cocok dengan penangan yang Anda deploy — sehingga Telegram mengirimkan jenis pembaruan yang tepat yang Anda tangani, dan tidak ada yang lain. Men-deploy penangan baru (atau menghapus satu) dapat membuat webhook tidak sinkron sampai disegarkan; npx tgcloud status juga menandai ini.

npx tgcloud webhook sync memperbaikinya — ia mengarahkan ulang webhook ke platform dan membangun kembali allowed_updates dari penangan yang Anda deploy. Tambahkan --drop-pending untuk membuang pembaruan yang sudah diantrekan Telegram sebelum sinkronisasi (jika tidak, mereka akan dikirim setelah webhook sehat kembali).

completion

Catatan: pelengkapan tab hanya berfungsi saat tgcloud polos ada di PATH Anda — jadi instal secara global (npm install -g @tgcloud/cli) atau masukkan binary tersebut ke PATH. Tidak bisa tersambung ke npx.

tgcloud completion <bash|zsh|fish>

Mencetak skrip pelengkapan shell ke stdout. Aktifkan sekali, lalu <Tab> akan melengkapi perintah, flag, direktori modul, jenis pembaruan penangan yang belum Anda miliki, dan modul yang dapat dijalankan secara lokal.

# bash — butuh paket bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — pastikan `autoload -U compinit && compinit` berjalan di ~/.zshrc Anda:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

Mulai ulang shell Anda (atau jalankan ulang file) setelahnya. Menjalankan tgcloud completion tanpa shell akan mencetak instruksi ini lagi.

upgrade

npx tgcloud upgrade [--dry-run]

Migrasi satu kali untuk proyek yang dibuat sebelum CLI 0.2.0, yang menyimpan schema.js, lib/, dan handlers/ di root proyek. Ia memindahkannya ke tgcloud/. Tautan Anda ke bot tetap terjaga, dan tidak ada yang perlu di-deploy ulang. --dry-run mencetak rencana perpindahan tanpa menyentuh apa pun. Perintah lain tidak berjalan di tata letak lama sampai Anda melakukan upgrade.

Tetap sinkron

Setiap proyek memiliki revisi yang meningkat secara monoton di cloud, bertambah pada setiap deploy. CLI mengingat revisi terakhir yang disinkronkan dengannya dan mengirimkannya pada setiap push. Jika cloud telah bergerak lebih jauh — karena mesin atau rekan tim lain melakukan deploy — push tersebut ditolak alih-alih diam-diam menimpa pekerjaan mereka, dan CLI menawarkan tiga cara ke depan:

npx tgcloud fetch           # lihat apa yang berubah di cloud, lalu periksa ulang
npx tgcloud pull            # bawa yang terbaru ke file kerja Anda
npx tgcloud push --force    # menimpa status cloud (berbahaya)

Pemeriksaan konkurensi-optimis inilah mengapa Anda dapat berbagi bot di seluruh tim tanpa proses deploy yang kaku. Perintah keluar non-nol saat gagal — deploy ditolak, migrasi gagal, kesalahan autentikasi, modul yang melemparkan error saat run — sehingga mereka dapat disusun dengan rapi dalam skrip dan pipeline CI.