Telegram Serverless stelt je in staat om backend-code voor je bot en Mini App rechtstreeks op de infrastructuur van Telegram uit te voeren — geen servers om in te richten, geen containers om actief te houden, en niet nadenken over schalen. Je schrijft eenvoudige JavaScript-modules, implementeert ze met één enkel commando, en Telegram voert ze uit in een snelle, geïsoleerde V8-sandbox die zich direct naast de Bot API en een ingebouwde database bevindt.
Als je ooit een bot hebt gekoppeld aan een VPS, een cloudfunctie of een hostingpaneel alleen maar om een /start te beantwoorden, dan is dit het gedeelte dat je niet langer hoeft te doen.
Waarom serverless
Een Telegram-bot is in de kern een programma dat reageert op updates. Traditioneel moest je dat programma ergens hosten waar het altijd aan, bereikbaar en veilig is — en dat vervolgens zo houden. Telegram Serverless verwijdert die laag volledig:
- Geen infrastructuur. Er is geen machine om te huren, te patchen of te monitoren. Je code draait op aanvraag en schaalt automatisch mee met je bot.
- Inclusief inloggegevens. De Telegram Bot API, een op SQLite gebaseerde database en uitgaand HTTP zijn direct beschikbaar voor elke module — niets om te installeren, geen inloggegevens om te koppelen.
- Snelle, geïsoleerde uitvoering. Elke aanroep draait in een lichtgewicht V8-isolatie, dicht bij de eigen systemen van Telegram, zodat aanroepen naar de Bot API en je database snel en betrouwbaar zijn.
- Een echte workflow voor ontwikkelaars. Een project leeft in een map op je computer onder versiebeheer. Je bewerkt bestanden, ziet precies wat er is veranderd, implementeert atomair en rolt je databaseschema vooruit met beoordeelde migraties — precies zoals je al met al het andere werkt.
Het mentale model: Hoe het project werkt
Je werkt op drie plaatsen, en ze komen keurig met elkaar overeen:
| Waar | Wat daar leeft |
|---|---|
| Je projectmap | JavaScript-modules — schema, gedeelde code, update-handlers, Mini App-endpoints — en, indien je die hebt, de front-end van de Mini App |
| De cloud | De geïmplementeerde kopie van die modules, de database van je bot en de bestanden van de Mini App |
De tgcloud CLI |
De brug — deze toont je de verschillen en synchroniseert ze |
Je hoeft nooit via SSH ergens in te loggen. Je bewerkt bestanden lokaal, voert npx tgcloud push uit, en het platform regelt de rest. Het verkeer van je bot wordt afgehandeld door de geïmplementeerde modules; je database blijft behouden tussen aanroepen door.
Een project heeft slechts vier soorten code, allemaal in één tgcloud/ map:
tgcloud/
├─ endpoints/ # functies die je Mini App aanroept
├─ handlers/ # functies die reageren op Telegram-updatetypen
├─ lib/ # gedeelde code die je overal kunt importeren
└─ schema.js # je databasetabellen
Wanneer er een update binnenkomt — een bericht, een druk op een knop, een inline query — stuurt Telegram deze naar de bijbehorende handler (handlers/message.js, handlers/callback_query.js, …) en roept de standaard export aan. Die functie communiceert via de SDK met de Bot API en de database, en keert terug. Dat is de hele lus. Een update zonder bijbehorende handler wordt simpelweg genegeerd, dus je voegt alleen de handlers toe die je nodig hebt.
Een Mini App is optioneel. Als je bot er een heeft, leeft de front-end ervan in hetzelfde project, naast tgcloud/. Serverless host de build-output en voert de endpoints/ uit die deze aanroept. Zie
Mini App front-end
.
Snelle demo
Hier is een complete, werkende demobot. Deze beantwoordt elk bericht en onthoudt hoeveel berichten uit elke chat zijn gezien.
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;
// Voeg de teller in, of verhoog deze als deze chat er al een heeft — en krijg de
// resulterende rij terug in hetzelfde statement via .returning().
const [row] = await db.insert(counters)
.values({ chatId, seen: 1 })
.onConflictDoUpdate({
target: counters.chatId,
set: { seen: sql`${counters.seen} + 1` },
})
.returning()
.run();
await api.sendMessage({
chat_id: chatId,
text: `Hallo! Ik heb ${row.seen} bericht(en) van je gezien.`,
});
}
Implementeren:
npx tgcloud push # upload de modules
npx tgcloud migrate # maak de `counters` tabel aan
Dat is een actieve bot met persistente status en zonder server. Alles erin — api, db, de table() DSL — wordt in de onderstaande secties beschreven.
Serverless is een algemene backend voor Telegram-bots en Mini Apps, niet een sjabloon voor één soort app. Het is ideaal voor:
- Conversatie-AI-bots die status per gebruiker in een database moeten opslaan.
- Mini App-backends die gebruikersgegevens opslaan en dynamische inhoud serveren.
- Games en tools — inclusief leaderboards, quizzen en meer.
- Automatiseringen en integraties die externe HTTP API’s aanroepen en resultaten in chats pushen.
Aan de slag
Deze handleiding neemt je mee van een lege map naar een actieve bot die berichten beantwoordt en gegevens opslaat. Er wordt vanuit gegaan dat je
Node.js
18 of nieuwer hebt geïnstalleerd en een bot hebt geregistreerd bij
@BotFather
. Aan het einde heb je elk commando gebruikt dat je dagelijks nodig hebt: push, migrate, run en status.
Zet voor alles eerst Serverless aan. Open in @BotFather je bot → Serverless en schakel het in. Dat activeert de functie voor deze bot en ontgrendelt de CLI-toegangstoken, handlers, bibliotheek en database.
1. Maak een project aan
De snelste manier om te beginnen is via de project-creator, die een project opzet en de CLI erin installeert:
npm create @tgcloud/bot example_bot
cd example_bot
Het argument is de doelmap: geef . door om in de huidige map te installeren, of een willekeurig pad. Het werkt ook in een bestaande map en overschrijft nooit bestanden die je al hebt.
Dit geeft je een project dat klaar is om bewerkt te worden:
example_bot/
├─ tgcloud/ # alles wat het platform uitvoert
│ ├─ handlers/
│ │ └─ message.js # een starter message-handler (echo't tekst terug)
│ ├─ lib/ # je gedeelde modules komen hier (leeg om te beginnen)
│ └─ schema.js # je databasetabellen
├─ docs/
│ └─ tgcloud-sdk.md # SDK-referentie (voor jou en je AI-tools)
├─ AGENTS.md # oriëntatie voor AI-code-assistenten
├─ package.json
└─ tgcloud.jsonc # projectconfiguratie
De gegenereerde bestanden zijn zelfdocumenterend — elk bestand bevat van commentaar voorziene voorbeelden van wat je daarna kunt doen.
De CLI installeert zichzelf in het project als een lokale dev-dependency, dus je voert deze uit met npx tgcloud <command> (npx vindt de kopie in de node_modules van je project), of via de npm run snelkoppelingen die de scaffold toevoegt aan package.json (npm run deploy, npm run status). Standaard is er geen globale tgcloud op je PATH.
Je kunt het ook globaal installeren — npm install -g @tgcloud/cli — als je liever een kale tgcloud vanaf elke locatie typt. Dat is handig om tgcloud init in een lege map uit te voeren, en het is wat shell tab-completion nodig heeft. Hoe dan ook krijg je hetzelfde project.
Bouw je een Mini App? Begin dan eerst bij de front-end. Maak deze aan met je gebruikelijke tool en voer vervolgens de project-creator uit in die map. Zie Mini App front-end .
2. Koppel je bot
Elk project is gekoppeld aan één bot. Verbind ze met login, waar gevraagd wordt om je CLI-toegangstoken (
@BotFather
→ je bot → Serverless → CLI Access → Access token — een ander token dan je
API-token van de bot
) en dit lokaal opslaat:
npx tgcloud login
Het token heeft de vorm app<id>:<secret>. De CLI bewaart dit in .tgcloud/, wat git-ignored is, en print nooit het geheime deel. Login is de enige keer dat erom gevraagd wordt — zie
Authenticatie
voor hoe tokens worden opgelost in CI.
3. Kijk rond
Twee commando’s vertellen je op elk moment hoe de zaken ervoor staan, beide volledig offline:
npx tgcloud status # wat is lokaal veranderd vs. de geïmplementeerde kopie
npx tgcloud diff # de regel-voor-regel wijzigingen
Direct na init is alles nieuw en is er nog niets geïmplementeerd. status toont de starter-bestanden die wachten om geüpload te worden.
4. Implementeren
Verzend je modules naar de cloud:
npx tgcloud push
push uploadt elke gewijzigde module in één atomaire batch en werkt je lokale overzicht bij van wat er nu in de cloud staat. Je bot is actief: open deze in Telegram en stuur een bericht — de starter-handler echo’t het terug.
Implementeren raakt je database nooit. Het pushen van code en het wijzigen van je databaseschema zijn bewust gescheiden stappen, zodat een code-implementatie je nooit kan verrassen met een datamigratie. Daar is de volgende stap voor.
5. Een databasetabel toevoegen
Laten we de bot iets laten onthouden. Open tgcloud/schema.js en declareer een 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())`) ,
});
Implementeer het schema en pas het vervolgens toe op de database:
npx tgcloud push # uploadt het nieuwe schema
npx tgcloud migrate # maakt de `messages` tabel aan
push meldt dat het schema niet synchroon loopt en toont je de in behandeling zijnde wijziging, maar past niets toe. migrate leidt je door de wijziging en, na jouw bevestiging, maakt de tabel aan. Dit tweestapsmodel — en wat er gebeurt bij risicovollere wijzigingen zoals drops — wordt behandeld in
Migraties
.
6. Gegevens opslaan en lezen
Gebruik nu de tabel vanuit je handler. Bewerk tgcloud/handlers/message.js:
import { api, db } from 'sdk';
import { eq } from 'sdk/db';
import { messages } from '../schema.js';
export default async function (message) {
// Sla dit bericht op.
await db.insert(messages)
.values({ chatId: message.chat.id, text: message.text })
.run();
// Tel hoeveel we er voor deze chat hebben opgeslagen.
const count = await db.$count(messages, eq(messages.chatId, message.chat.id));
await api.sendMessage({
chat_id: message.chat.id,
text: `Opgeslagen. Dat zijn tot nu toe ${count} bericht(en) uit deze chat.`,
});
}
Implementeer de bijgewerkte handler met npx tgcloud push, stuur je bot vervolgens een paar berichten en zie hoe de teller oploopt. De database blijft behouden tussen aanroepen door — dat is het geheugen van je bot.
7. Testen zonder te implementeren
Je hoeft niet te implementeren om een wijziging te proberen. npx tgcloud run voert een handler uit op het platform met behulp van je lokale bestanden, zonder ze te publiceren:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hallo" }'
Het argument is de payload die je handler ontvangt — voor handlers/message een
Message
— geschreven in
JSON5
(dus je kunt de aanhalingstekens bij keys overslaan). Het commando print alles wat de handler heeft gelogd met console.*, de retourwaarde en hoe lang het duurde. Dit is de snelste lus voor het itereren op logica — geen implementatie, niet wachten op een echt bericht.
8. Gesynchroniseerd blijven
Terwijl je werkt, houdt een handvol commando’s je lokale project en de cloud op één lijn: npx tgcloud status laat zien wat is veranderd, npx tgcloud push implementeert, npx tgcloud pull brengt je lokale project op één lijn met de cloud, npx tgcloud fetch controleert wat er geïmplementeerd is zonder je bestanden aan te raken, en npx tgcloud reset verwijdert lokale wijzigingen.
Als twee mensen (of twee machines) naar dezelfde bot implementeren, detecteert het platform het conflict en stopt
pushom je eerst te latenpullen — je kunt nooit stilletjes iemands werk overschrijven. Zie Gesynchroniseerd blijven .
Bouwen met AI
Bouw je liever met een AI-assistent — of is de enige programmeur in je team een AI? Je kunt nog steeds een bot verzenden. We hebben een eerste stap gezet om een AI-agent zich thuis te laten voelen in een project: elk nieuw project wordt opgezet met een AGENTS.md en een docs/tgcloud-sdk.md referentie die AI-codeertools automatisch lezen.
Samen met een kleine, op zichzelf staande runtime — één SDK, geen npm-pakketten om te beheren — geeft dat de assistent een vliegende start met de conventies die generieke codegeneratoren hier vaak missen: relatieve imports met .js, geen foreign keys, elke db-aanroep is async, één handler per updatetype, en de tweestaps push/migrate-flow.
Probeer het:
npm create @tgcloud/bot mijn-bot
cd mijn-bot
opencode # of Claude Code, Cursor, … — elke agent die AGENTS.md leest
Vraag dan gewoon, in duidelijke taal:
Schrijf een bot die de takenlijst van elke persoon onthoudt — voeg een item toe wanneer ze tekst sturen, en toon de hele lijst wanneer ze /list sturen.
De assistent bewerkt schema.js en je handlers voor je; jij beoordeelt, test een wijziging direct met npx tgcloud run, en gaat dan live met npx tgcloud push en npx tgcloud migrate. AGENTS.md maakt deel uit van je project — bewerk het naarmate de bot groeit zodat de begeleiding accuraat blijft.
Onderweg met BotFather
Alleen je telefoon bij de hand? Het hele project leeft ook in @BotFather — open je bot → Serverless en je krijgt alles wat de CLI beheert, op een touchscreen:
- Handlers — maak, bewerk en test update-handlers; BotFather houdt de webhook synchroon met de handlers die je hebt (hetzelfde In sync / Out of sync als de CLI rapporteert).
- Endpoints — de functies die je Mini App aanroept.
- Bibliotheek — je gedeelde
lib/-modules. - Database — bewerk
schema.jsin dezelfde Drizzle-achtige syntaxis, beoordeel in behandeling zijnde wijzigingen en pas ze toe; Opslaan implementeert ze. - Mini App-bestanden — de front-end die je gebruikers openen in Telegram.
- CLI-toegang — pak hier de CLI-toegangstoken wanneer je weer bij een toetsenbord bent.
Het is een en hetzelfde cloudproject, dus je kunt een handler op je telefoon starten en deze later met npx tgcloud pull naar je laptop halen — niets is gebonden aan één client. Het uitvoeren van een handler toont zelfs de Console-output direct in de chat, net als
npx tgcloud run
.
Projecten en modules
Een serverless-project is een gewone map onder versiebeheer. Alles wat het platform uitvoert staat in één map, tgcloud/; de rest van het project is van jou — een mini-app front-end, configuratie, docs. Er is geen build-stap voor de bot, geen node_modules tijdens runtime, en geen server-entrypoint.
Anatomie van een project
voorbeeld_bot/
├─ tgcloud/ # het geïmplementeerde deel — niets anders
│ ├─ handlers/ # update-handlers — plat, slechts één niveau
│ │ ├─ message.js
│ │ └─ callback_query.js
│ ├─ endpoints/ # functies die je Mini App aanroept — plat, slechts één niveau
│ │ └─ getProfile.js
│ ├─ lib/ # gedeelde modules; submappen toegestaan
│ │ ├─ reply.js
│ │ └─ internal/util.js
│ └─ schema.js # databaseschema — één bestand
└─ .tgcloud/ # CLI-status, git-ignored
Alleen .js-bestanden in tgcloud/ — schema.js, lib/, handlers/, endpoints/ — worden geïmplementeerd. Al het andere — Markdown, configuratiebestanden, de .tgcloud/-map, alles buiten tgcloud/ — blijft op je machine. Modulenamen zijn relatief ten opzichte van tgcloud/. Het bestand tgcloud/handlers/message.js is de module handlers/message, en dat is de naam die npx tgcloud run gebruikt.
Upgraden vanaf een ouder project? Projecten die zijn gemaakt vóór CLI 0.2.0 hielden
schema.js,lib/enhandlers/in de root van het project. Voer eenmaalnpx tgcloud upgradeuit om ze naartgcloud/te verplaatsen.
schema.js — je database. Het declareert tabellen als named exports met behulp van de schema-DSL en leeft in tgcloud/ als één enkel bestand. Het wordt geïmplementeerd zoals elke andere module, maar het implementeren ervan wijzigt de database nooit — schemawijzigingen worden afzonderlijk toegepast met npx tgcloud migrate. Zie
De database
.
lib/ — gedeelde code, alles wat je wilt hergebruiken in handlers: pure helpers, database-toegangslagen, formattering, integraties met externe diensten. lib/ is de enige map die submappen mag bevatten (lib/internal/util.js, lib/payments/stripe.js), zodat je een grotere codebase kunt organiseren zoals je wilt. Modules in lib/ worden nooit direct door het platform aangeroepen; ze bestaan om te worden geïmporteerd door handlers en door elkaar.
handlers/ — de entrypoints van je bot. Elk bestand komt overeen met één Telegram-updatetype, en het platform stuurt elke inkomende update naar de overeenkomstige handler:
| Bestand | Behandelt |
|---|---|
handlers/message.js |
Nieuwe inkomende berichten |
handlers/inline_query.js |
Nieuwe inkomende inline queries |
handlers/callback_query.js |
Nieuwe inkomende callback-queries |
| … | elk ander Bot API-updatetype |
handlers/ is plat — geen submappen. De export default van een handler is de functie die het platform aanroept (zie
Handlers
).
Een updatetype wordt alleen behandeld als het handler-bestand bestaat en niet leeg is. Als er geen handlers/<type>.js is — of het bestand is leeg — worden updates van dat type genegeerd en voert het platform niets uit voor hen. Dus bewaar alleen de handlers die je echt nodig hebt: elke handler die je toevoegt is een ander updatetype waar je bot wakker voor wordt, en het weglaten van de rest betekent dat Telegram je code niet opstart voor updates die je toch zou negeren.
Om een nieuwe handler op te zetten, voer je
npx tgcloud add handlers/<type>uit.
endpoints/ — de server-kant van je Mini App. Elk bestand is een functie die de Mini App bij naam aanroept, dus endpoints/getProfile.js beantwoordt POST /api/getProfile. Het platform controleert of de aanroep afkomstig is van een Telegram-gebruiker binnen je Mini App voordat je code draait, en geeft je die gebruiker. endpoints/ is plat, net als handlers/. Zie
Endpoints
.
.tgcloud/ — machine-lokale status die volledig door de CLI wordt beheerd, inclusief je opgeslagen inloggegevens. Het is git-ignored, en je moet het nooit handmatig lezen of bewerken — gebruik in plaats daarvan de CLI-commando’s.
Het modulesysteem
Tijdens runtime kan een module precies twee dingen zien: de platform-SDK en de andere modules in je project. Er zijn geen npm-pakketten, geen bestandssysteem en geen netwerk behalve via de fetch van de SDK.
Importeer je eigen modules zoals je dat in elk JavaScript-project zou doen — via een relatief pad, met de .js-extensie — en de SDK op naam:
// tgcloud/handlers/message.js
import { users } from '../schema.js'; // de schemamodule
import { addItem } from '../lib/cart.js'; // een lib-module
import { format } from '../lib/internal/fmt.js'; // geneste lib-module
import { db, api, fetch } from 'sdk'; // de platform-SDK
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js'; // een buur-module
import { users } from '../schema.js';
Een relatieve import moet eindigen op .js en mag niet buiten tgcloud/ leiden:
import { users } from '../schema'; // voeg de .js toe
import config from '../../config.js'; // buiten tgcloud/
import { z } from 'zod'; // geen npm-pakketten
Elke module heeft ook een naam — het pad binnen tgcloud/ zonder de extensie, zoals schema, lib/cart of handlers/message. Die naam is wat npx tgcloud run en de output van de CLI gebruiken, en een module kan er ook mee worden geïmporteerd (from 'lib/cart', geen extensie). Geef de voorkeur aan relatieve imports in je code. Dat is wat je editor begrijpt — jump-to-definition, autocomplete, hernoemen.
Wat beschikbaar is tijdens runtime zijn precies twee dingen: sdk en zijn submodules (sdk/db, sdk/api, sdk/fetch) — het hele platformoppervlak, zie
De SDK
— en je eigen modules in tgcloud/. Dat is de volledige lijst. Als je code iets anders importeert, zal het niet oplossen. Deze beperking is wat modules snel te laden en veilig uit te voeren houdt.
Handlers
Een handler is een module in handlers/ waarvan de default export door het platform wordt aangeroepen wanneer een bijbehorende update binnenkomt.
// handlers/message.js
import { api } from 'sdk';
export default async function (message) {
await api.sendMessage({
chat_id: message.chat.id,
text: `Je zei: ${message.text ?? '(geen tekst)'}`,
});
}
Een handler ontvangt de payload van de update als argument — het platform pakt de Telegram
Update
voor je uit. handlers/message.js krijgt het
Message
-object (d.w.z. update.message); handlers/callback_query.js krijgt de
CallbackQuery
; enzovoort. Het tweede argument van de handler is een contextobject per aanroep, ctx. Het bevat de ruwe Update als ctx.update — gebruik dit wanneer je iets buiten de payload nodig hebt, zoals update_id.
Een handler kan async zijn (meestal zo) en kan een waarde retourneren. Het bereikt de Bot API, de database en uitgaand HTTP via de SDK.
Je hebt geen echte update nodig om een handler te testen. npx tgcloud run voert deze op het platform uit met de payload die je levert en je huidige lokale code:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
Het argument is de payload — hetzelfde object dat je handler ontvangt — in JSON5. Om de ctx (het tweede argument) van de handler te leveren, voeg je --ctx toe, bijv. --ctx '{ update: { update_id: 1 } }'. Dit draait tegen je lokale bestanden, zodat je wijzigingen kunt proberen voordat je ze implementeert. Zie
run
.
Endpoints
Een endpoint is een module in endpoints/ die je Mini App aanroept — de plek voor alles wat de front-end niet zelf kan, zoals het lezen en schrijven van de database, het aanroepen van de Bot API, of praten met externe diensten met je geheimen.
// 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('Profiel niet gevonden', { userId: user.id });
return profile;
}
Het eerste argument, input, is het JSON-object dat de Mini App heeft verzonden. Het tweede, ctx, bevat ctx.initData — de
init-data
van de Mini App (user, chat, start_param, auth_date, …), reeds geverifieerd door het platform. Het heeft dezelfde vorm als Telegram.WebApp.initDataUnsafe op de client, maar hier kun je het vertrouwen. Een aanroep zonder geldige init-data wordt afgewezen met 401 voordat je code draait, dus ctx.initData.user is echt de persoon die je Mini App heeft geopend. Endpoints kunnen alleen op deze manier worden aangeroepen — vanuit de Mini App van je bot, door een Telegram-gebruiker — dus er is geen publieke API om te beschermen en geen login om te bouwen.
Alles wat het endpoint retourneert, wordt als JSON teruggestuurd. Om een aanroep te weigeren, throw new EndpointError(beschrijving, parameters?). De Mini App krijgt de beschrijving en het parameters-object zoals ze zijn. Als je in de front-end moet vertakken op het soort fout, plaats dan een code in parameters — { code: 'OUT_OF_STOCK' }. Elke andere uitzondering wordt gerapporteerd als een generieke interne fout, en het echte bericht gaat alleen naar je logs, zodat internals nooit uitlekken naar de client.
Een endpoint aanroepen vanuit de Mini App
De Mini App roept een endpoint aan op naam met Telegram.WebApp.Serverless.call uit de officiële
Mini Apps SDK
, telegram-web-app.js — het script dat je Mini App al bevat. Het verzendt de init-data voor je en roept terug met een fout of het resultaat van het endpoint:
Telegram.WebApp.Serverless.call('getProfile', { lang: 'nl' }, function (err, profile) {
if (err) {
// err.message — de beschrijving; err.parameters — wat je EndpointError bevatte
if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
return;
}
render(profile);
});
De input is optioneel en moet een object zijn. Zonder is de aanroep Serverless.call('getProfile', callback).
err.type is 'ENDPOINT_ERROR' wanneer je endpoint een EndpointError heeft gegooid. Elke andere waarde betekent dat het platform de aanroep heeft geweigerd of dat het endpoint onverwacht is mislukt, bijvoorbeeld 'UNAUTHORIZED' wanneer de init-data ontbreekt of ongeldig is.
Endpoint-namen zijn als JavaScript-identifiers — letters, cijfers en _, niet beginnend met een cijfer (getProfile, place_order). Zet er een op met
npx tgcloud add endpoints/<naam>
, en probeer het zonder de Mini App met
run
, waarbij je de init-data zelf doorgeeft:
npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1, first_name: "Ann" } } }'
Wat wordt geïmplementeerd
Wanneer je npx tgcloud push uitvoert, verzamelt de CLI elk .js-bestand onder schema.js, lib/, handlers/ en endpoints/, en verzendt die exacte set als de module-ruimte van je project — samen met de build-map van de Mini App, als tgcloud.jsonc er een benoemt (zie hieronder). Alles wat aanwezig is in de cloud maar afwezig in je project wordt verwijderd, zodat de geïmplementeerde status altijd je map weerspiegelt — inclusief verwijderingen. Niets buiten tgcloud/ wordt als code geïmplementeerd — de rest van de project-root is van jou. Binnen tgcloud/ wordt een .js-bestand dat niet op een van die locaties staat (tgcloud/util.js, tgcloud/helpers/x.js) gemarkeerd, zodat het niet onbedoeld niet-geïmplementeerd blijft. Markdown, dotfiles en .tgcloud/ worden nooit geïmplementeerd.
Mini App front-end
De project-root is van jou, en het is de natuurlijke plek voor de front-end van je Mini App — gebouwd met React, Vue, Svelte of gewone HTML, door Vite of een andere bundler. Serverless host de build-output naast je bot. Eén push implementeert de code en de statische bestanden samen, en de app wordt geserveerd op https://app<app_id>.tgcloud.ai/ (push print het exacte adres — gebruik dit als de URL van je Mini App in
@BotFather
).
Misbruik melden. Mini Apps op tgcloud.ai worden gepubliceerd door onafhankelijke ontwikkelaars. Om illegale inhoud of misbruik op een
app<app_id>.tgcloud.aisubdomein te melden, e-mail [email protected] .
Om te beginnen, maak je eerst de front-end aan met de tool die je wilt, en voeg je daarna Serverless toe:
npm create vite@latest example_app # of create-vue, …
cd example_app
npm create @tgcloud/bot .
De project-creator overschrijft nooit je bestanden. Het voegt de tgcloud/-map toe naast de front-end, herkent de build-map en voegt een npm run deploy-script toe dat de app bouwt en samen met de bot implementeert.
Wat wordt gehost, is ingesteld in tgcloud.jsonc, het configuratiebestand van het project in de root:
{
"$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 }]
}
}
Alleen "source" is verplicht. Het is de build-map, relatief ten opzichte van de root van het project. De andere keys van "static" configureren hoe het wordt geserveerd (hieronder). init maakt tgcloud.jsonc voor je aan. Wanneer het de front-end herkent, vult het ook "source" in:
| Front-end | init zet "source" op |
|---|---|
| Vite — React, Vue, Svelte, Solid, … | dist |
| Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular | niets — configureer eerst een statische export, en zet "source" vervolgens zelf op die map |
Configureer bij elke andere setup "source" zelf op je build-map.
De $schema-regel geeft je editor autocomplete en validatie voor elke key. Het bestand is JSON met commentaar en trailing commas toegestaan. Als een andere tool van je het als gewone JSON moet lezen, noem het dan tgcloud.json in plaats daarvan — dan moet het strikte JSON zijn.
Bouw voordat je pusht. push uploadt de map exact zoals deze op de schijf staat — behalve bestanden en mappen waarvan de naam begint met een punt, die worden overgeslagen — en voert nooit je build uit, dus een gewone npx tgcloud push na het bewerken van de front-end zou de vorige build implementeren. Gebruik npm run deploy (het voert je build-script uit, dan tgcloud push), of bouw zelf eerst.
De serveeropties:
spa— serveerindex.htmlvoor paden die niet bestaan en geen bestandsextensie hebben, zodat een client-side router werkt bij een herlaadbeurt. Paden met een extensie krijgen nog steeds404.immutable— patronen voor content-gehashte bestanden (Vite’s/assets/*) die browsers een jaar mogen cachen. Gebruik het alleen voor content-gehashte bestanden. Elk ander bestand dat hierbij past, zou een jaar lang verouderd blijven in browsers, en er is geen manier om het terug te roepen.headers— response-headers voor de paden die een regel matcht (een exact pad, of een prefix eindigend op/*). Eennull-waarde verwijdert een header.redirects— redirect-regels, waarbij de eerste match wint. Een bestaand bestand wint altijd van een regel.statusis301tenzij je het anders instelt."static": "dist"— een afkorting voor{ "source": "dist" }wanneer je niets anders nodig hebt."static": false— verwijder de geïmplementeerde site bij de volgende push. Het verwijderen van de key stopt er alleen mee dat dit project statische bestanden beheert, en alles wat is geïmplementeerd blijft staan.
De site wordt geserveerd vanaf de root van zijn eigen host, dus de standaard base (/) van je bundler is correct. Telegram Web opent Mini Apps in een iframe, dus voeg geen X-Frame-Options of een restrictief frame-ancestors-beleid toe.
Dezelfde host beantwoordt je
endpoints
onder /api/ — daar gaat Telegram.WebApp.Serverless.call('getProfile', …) heen. Plaats geen eigen bestanden onder /api/ in de build-map.
De database
Elke bot krijgt zijn eigen database — een op SQLite gebaseerde opslag die behouden blijft tussen aanroepen en beschikbaar is voor elke module via db. Je beschrijft je tabellen in schema.js met een kleine, getypeerde DSL; je leest en schrijft ze met een vloeiende query builder; en je evolueert ze met beoordeelde migraties.
Ken je Drizzle ORM ? Dan weet je al hoe je hier met de database praat. De schema-DSL en query builder volgen Drizzle — de column builders,
select().from().where(), de operatoren,onConflictDoUpdate,.returning()en desql-tag gedragen zich allemaal zoals je zou verwachten, dus het lezen en schrijven van gegevens is de vertrouwde API die je al gebruikt. Je importeert het gewoon vanuitsdk/db, en enkele platform-specifieke zaken (vooral geen foreign keys ) worden aangegeven waar ze voorkomen.
Tabellen declareren
Tabellen zijn named exports in schema.js. Het aanroepen van table() bouwt een beschrijving tijdens het laden (het raakt de database niet aan); het platform ontdekt de geëxporteerde tabellen wanneer je schema.js implementeert en migreert de database zodat deze overeenkomt.
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(naam, kolommen, extra?):
naamis de SQL-tabelnaam;kolommenis een map van JS-eigenschap → kolomdefinitie;extrais een optionele callback(t) => ({ … })waartde kolommen blootstelt (t.createdis een verwijzing naar die kolom) — declareer hier indexen en tabel-niveau restricties.
Kolomtypes
| Factory | SQLite-type | Notities |
|---|---|---|
text() |
TEXT | |
integer() |
INTEGER | |
real() |
REAL | alias float() |
numeric() |
NUMERIC | |
blob() |
BLOB | leest/schrijft Uint8Array |
boolean() |
INTEGER | opgeslagen als 0/1, gelezen als true/false |
json() |
TEXT | auto JSON.stringify / JSON.parse |
Het argument voor de kolomnaam is optioneel — laat het weg en de JS-key wordt gebruikt. De
mode-optie regelt hoe waarden converteren tussen SQLite en JavaScript.
mode |
Opgeslagen als | JS-waarde |
|---|---|---|
boolean |
INTEGER 0/1 | boolean |
json |
TEXT (JSON) | elk object/array |
timestamp |
INTEGER (unix seconden) | Date |
timestamp_ms |
INTEGER (unix ms) | Date |
bytes |
BLOB | Uint8Array |
boolean() en json() zijn afkortingen voor integer(name, { mode: 'boolean' }) en text(name, { mode: 'json' }).
Een blob() leest en schrijft een Uint8Array — de runtime heeft geen Node Buffer (en een Buffer is een Uint8Array-subklasse, dus dit is het draagbare basistype). De mode hierboven regelt alleen hoe een waarde wordt geëncodeerd, onafhankelijk van het opslagtype van de kolom: dus blob('col', { mode: 'json' }) is de BLOB-tegenhanger van json() — dezelfde JSON-encodering, bewaard in een BLOB-kolom in plaats van TEXT.
TLDR:
blob()gebruiktUint8Array.moderegelt encodering, niet opslag, dusblob(..., { mode: 'json' })slaat JSON op als een BLOB, terwijljson()het als TEXT opslaat.
Kolommodifiers
Kolommodifiers koppelen aan een 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' }) // of 'virtual'
text('email').deprecated('vervangen door login') // markeert de kolom voor verwijdering
.default()op eenjson()-kolom encodeert de waarde voor je; eensql`…`-default wordt letterlijk doorgegeven..deprecated()is terminaal — zie Migraties .
Indexen en restricties
Indexen en restricties worden gedeclareerd in de extra-callback, waar de kolommen in scope zijn.
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})`) , // expressie-index
active: index('idx_active').on(t.userId).where(sql`done = 0`) , // gedeeltelijke index
}));
Tabel-niveau modifiers ketenen na table(...): .strict(), .withoutRowid(), .deprecated('reden').
Geen foreign keys
De runtime draait met PRAGMA foreign_keys uit. Een gedeclareerde foreign key zou stilzwijgend inactief zijn — geen cascades, geen bescherming tegen wezen — wat erger is dan er helemaal geen hebben, dus de DSL maakt het onmogelijk. Namelijk, .references() en tabel-niveau foreignKey() gooien een fout bij declaratie, en een schema dat ze gebruikt zal niet implementeren.
Je moet relaties modelleren met gewone kolommen (userId: integer('user_id')) en integriteit afdwingen in je applicatiecode: voeg ouders in vóór kinderen, verwijder kinderen vóór ouders, behandel fouten correct en ruim wezen op met een LEFT JOIN … WHERE parent.id IS NULL wanneer je dat nodig hebt.
De afwezigheid van foreign keys is een bewuste beperking, geen toezicht. Houd er vroeg rekening mee bij het plannen van je bot.
Querying
db is een vloeiende query builder. Elke query is asynchroon — de terminale methoden (.all(), .get(), .values(), .run()) retourneren Promises, dus await altijd.
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(); // alle rijen
await db.select().from(todos).where(eq(todos.id, 1)).get(); // eerste rij of null
await db.select().from(todos).values(); // rijen als waarde-arrays
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();
// custom projectie: { 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();
// rij-telling — een helper, geen builder-terminal:
await db.$count(todos); // alle rijen
await db.$count(todos, eq(todos.done, false)); // met een filter
- Chainable:
.where(),.orderBy(),.limit(),.offset(),.groupBy(),.having(),.distinct(). - Terminalen:
.all(),.get(),.values(). - Tel rijen met
db.$count(table, where?)(ofcount()in een projectie).
Invoegen, bijwerken, verwijderen:
await db.insert(todos).values({ userId: 1, text: 'Koop melk' }).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();
Merk op dat een batch-insert één statement is, dus deze is beperkt door de variabelelimiet van SQLite (
rijen × kolommen); als je eroverheen gaat, mislukt de insert — hak het zelf in stukken.
Operatoren worden geïmporteerd vanuit 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) met meerdere argumenten is hetzelfde als and(a, b). Het tweede argument van een vergelijking is standaard een waarde, maar kan een andere kolom zijn of sql`…` — bijv. eq(a.x, b.y). Aggregaten (count/sum/avg/min/max) zijn sql-fragmenten voor een .select({ … })-projectie.
Ruwe SQL — wanneer de builder niet genoeg is, val terug op ruwe SQL. De modus wordt gekozen per methode: db.run voor schrijfacties, db.all voor veel rijen, db.get voor één rij, db.values voor rijen als positionele arrays.
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, 'koop melk']]
Een schrijfactie resulteert in een run-resultaat — rowsAffected, lastInsertRowid en rows, die een RETURNING-clausule bevatten als de SQL er een had. Gebruik db.values wanneer een query twee kolommen met dezelfde naam projecteert (een join zoals SELECT u.id, o.id): db.all keyt rijen op kolomnaam, dus de tweede id zou de eerste overschrijven.
De sql`…` -tag verandert ${value} in een gebonden parameter en ${table.column} in een identifier, en plakt geneste sql-fragmenten aan elkaar. Gebruik sql.raw('…') voor een letterlijke waarde zonder parameters.
Ruwe queries zijn niet gebonden aan een tabel, dus hun rijen komen terug zonder mode-conversie — een boolean is
0/1, een JSON-kolom is een string, een timestamp is een getal. Alleen de tabel-gebonden builder converteert waarden.
Migraties
Je database verandert naarmate je bot groeit. Het platform houdt dat veilig door het implementeren van code te scheiden van het wijzigen van gegevens, en door elke schemawijziging te classificeren op basis van hoe riskant deze is.
Implementeren raakt de database nooit. Wanneer je npx tgcloud push uitvoert op een gewijzigde schema.js, legt het platform het nieuwe schema vast en vertelt je wat de database zou veranderen — maar past niets toe:
npx tgcloud push # implementeer schema.js; rapporteert in behandeling zijnde DB-wijzigingen
npx tgcloud migrate # beoordeel en pas ze toe
npx tgcloud migrate berekent het verschil tussen je schema en de live database en leidt je erdoorheen. Er wordt altijd gevraagd voordat er iets wordt toegepast. Dit betekent dat een routine code-implementatie nooit per ongeluk een datamigratie kan activeren.
Elke in behandeling zijnde wijziging heeft een status die bepaalt hoe migrate ermee omgaat:
| Status | Betekenis | In migrate |
|---|---|---|
| safe | Additief en niet-blokkerend — een nieuwe tabel, kolom of index | Samen toegepast in één stap, na bevestiging |
| warning | Potentieel destructief of traag — iets verwijderen, of een index op een enorme tabel | Eén voor één gepresenteerd, elk afzonderlijk bevestigd |
| manual | Kan niet automatisch worden gedaan — bijv. het wijzigen van het type van een kolom | Getoond met begeleiding; je voert het met de hand uit |
| undocumented | Bestaat in de database maar niet in je schema | Getoond ter bewustwording; niet toegepast |
Veilige wijzigingen zijn snel en in de geest omkeerbaar, dus ze worden samen doorgevoerd. Elke waarschuwing is een bewuste, individuele bevestiging — er is geen “alles toepassen” voor destructieve wijzigingen.
Handmatige wijzigingen komen met een reden en een voorgestelde actie. migrate eindigt met een samenvatting: hoeveel wijzigingen zijn toegepast, overgeslagen, wachtend op een handmatige fix, of niet in je schema.
Zie
migratevoor de vlaggen (--dry-run,--safe,--yes,--local).
Dingen verwijderen
Het verwijderen van een tabel of kolom uit schema.js verwijdert deze niet — dat zou een per ongeluk verwijderen catastrofaal maken. Om iets te verwijderen, markeer je het als deprecated:
// verwijder een kolom
text('email').deprecated('vervangen door login')
// verwijder een hele tabel
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('ongebruikt');
Bij de volgende migrate verschijnt het deprecated object als een warning-status drop die je individueel bevestigt. Zodra het weg is, verwijder je de declaratie.
Het type van een kolom wijzigen
Typewijzigingen zijn handmatig — SQLite kan ze niet altijd ter plekke doen, en het dwingen van bestaande waarden is een kwestie van oordeel. migrate toont de wijziging en de redenering; voer het zelf uit met ruwe SQL (db.run(...)), meestal door een nieuwe kolom of tabel aan te maken, gegevens te kopiëren en te wisselen.
De SDK
Tijdens runtime heeft een module één bibliotheek: sdk. Het bundelt de drie dingen die een bot-backend nodig heeft — een database, de Telegram Bot API en uitgaand HTTP — met niets om te installeren en geen inloggegevens om te configureren. De database (db) wordt behandeld in
De database
; deze sectie behandelt api, fetch en de console-global.
import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk'; // het hele oppervlak
// of vanuit submodules:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
| Import | Wat het is |
|---|---|
db |
Database — query builder en schema DSL → De database |
api |
Telegram Bot API — api.sendMessage(...) →
hieronder
|
fetch |
Uitgaand HTTP → hieronder |
InputFile |
Een bestand (bytes + naam) om te uploaden — werkt met zowel api als fetch →
Bestanden
|
EndpointError |
Weiger een Mini App-aanroep met een bericht dat deze kan tonen → Endpoints |
Importeer je eigen projectmodules via een relatief pad met de .js-extensie (from '../schema.js', from '../lib/cart.js'). Zie
Het modulesysteem
.
De Bot API
api geeft je de volledige
Telegram Bot API
. Roep elke methode aan als api.<methode>(params). Elke huidige — en toekomstige — Bot API-methode werkt zonder SDK-update vereist.
import { api } from 'sdk';
const me = await api.getMe(); // → het uitgepakte resultaat
await api.sendMessage({ chat_id: id, text: 'Hallo!' });
await api.editMessageText({ chat_id, message_id, text: 'Bijgewerkt' });
await api.answerCallbackQuery({ callback_query_id, text: 'Klaar' });
De response-envelope is uitgepakt. De Bot API verpakt resultaten normaal gesproken in { ok: true, result: … }. api retourneert het result direct — getMe() lost op naar het gebruikersobject, niet naar een wrapper. Parameters gebruiken de eigen snake_case namen van de Bot API (chat_id, message_id, reply_markup, …).
Fouten gooien BotApiError. Wanneer de Bot API { ok: false } retourneert, gooit de aanroep een BotApiError in plaats van een falsy waarde te retourneren, zodat je deze niet per ongeluk kunt negeren. De fout bevat .code (de Bot API error_code), .description (het leesbare bericht), .method (welke methode mislukte) en .parameters (extra gegevens zoals retry_after bij een 429, of migrate_to_chat_id). Vang deze om een verwachte fout af te handelen en gooi de rest opnieuw:
import { api, BotApiError } from 'sdk';
try {
await api.deleteMessage({ chat_id, message_id });
} catch (e) {
if (e instanceof BotApiError && e.code === 400) {
// 400 = het bericht is al weg; dat is hier prima.
} else {
throw e;
}
}
Bestanden
Werk met bestandsbytes, niet alleen met file_ids — upload nieuwe bestanden en download bestaande.
Upload — InputFile. Verpak ruwe bytes in een InputFile (een eenvoudige houder: bytes, een bestandsnaam en een optioneel MIME-type) en geef deze door waar een Bot API-methode een bestand verwacht:
import { api, InputFile } from 'sdk';
const doc = new InputFile(bytes, 'rapport.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });
// bestanden genest in params werken ook — bijv. een 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' }) },
]});
De bytes worden naar Telegram gestreamd als een echte multipart-upload — het bestand wordt niet volledig in één aanroep naar het platform gebufferd — maar de Bot API-limieten van Telegram zijn nog steeds van toepassing: een bot kan momenteel tot 50 MB per bestand uploaden (en 10 MB voor een foto).
Download — per file_id. Twee helpers halen de bytes van een bestand terug. Het platform lost de file_id voor je op (het voert getFile uit en haalt de inhoud achter de schermen op), dus je raakt nooit het bot-token of een download-URL aan:
const bytes = await api.getFileContent(file_id); // → Uint8Array (het hele bestand)
const { file, body } = await api.getFileStream(file_id); // stream het in plaats daarvan
file.file_unique_id; // de getFile info
for await (const chunk of body) { /* Uint8Array */ } // of buffer het: await stream.bytes()
De Bot API van Telegram begrenst
getFile-downloads op 20 MB, dus dat is het plafond voor beide helpers.
HTTP
fetch is een fetch-achtige client voor het aanroepen van de buitenwereld — externe API’s, webhooks, alles via HTTP.
import { fetch } from 'sdk';
const res = await fetch('https://api.voorbeeld.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();
De respons weerspiegelt het webplatform: res.status, res.statusText, res.ok (true voor 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) en body-readers await res.json() / await res.text().
Je kunt de body ook incrementeel lezen als een stream —
for await (const chunk of res.body) { … }— wat de manier is waarop je server-sent events of token-voor-token output van AI API’s verbruikt.
Body-helpers zetten de bijpassende Content-Type voor je:
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
Request-bodies kunnen ook bestanden en ruwe bytes bevatten: geef een Uint8Array, een InputFile of een FormData (voor multipart/form-data) door als body, en de bytes worden gestreamd — zodat je op dezelfde manier naar een externe API kunt uploaden als je bestanden naar Telegram stuurt:
import { fetch, FormData, InputFile } from 'sdk';
const form = new FormData();
form.append('file', new InputFile(bytes, 'foto.jpg', { type: 'image/jpeg' }));
form.append('caption', 'van mijn bot');
const res = await fetch('https://api.voorbeeld.com/upload', { method: 'POST', body: form });
Het gedraagt zich verder als de standaard fetch die je al kent, met twee beperkingen:
- De respons wordt gelezen als tekst —
await res.json()/await res.text(), of gestreamd chunk-voor-chunk metres.body. - De totale respons is begrensd op 30 MB. Die limiet dekt de hele respons — streamen met
res.bodylaat je een grote body incrementeel verwerken, maar het verhoogt de limiet niet.
Logging
De standaard globale console is beschikbaar — niets om te importeren, het is er gewoon zoals in elke JavaScript. De output wordt vastgelegd en getoond door npx tgcloud run, wat het je primaire debugging-tool maakt tijdens de ontwikkeling.
console.log('verwerken', { chatId: id }); // log / debug — gewoon
console.info('gestart'); // info — blauw
console.warn('rate limited'); // warn — geel
console.error(err); // error — rood, met een stack trace
Elke regel is getagd met de [bestand:regel] waar deze vandaan komt. console.error en console.trace voegen een volledige stack toe, terwijl console.warn dat niet doet. Wanneer je npx tgcloud run op een module uitvoert, worden deze regels geprint met een gekleurd prefix per niveau, de tijd sinds het begin van de run en de oorsprong — zie
run
.
Command-line interface
tgcloud is de brug tussen je projectmap en de cloud. Het zet projecten op, laat zien wat er veranderd is, implementeert, voert modules uit en past databasemigraties toe. Het vereist
Node.js
18 of nieuwer. Twee manieren om het te krijgen:
# Aanbevolen — maak een project met de CLI erin geïnstalleerd:
npm create @tgcloud/bot voorbeeld_bot
# Of installeer de CLI globaal en initieer een lege map:
npm install -g @tgcloud/cli
tgcloud init
Het npm-pakket is @tgcloud/cli en het commando dat het installeert is tgcloud. Elk commando werkt vanuit elke submap van je project.
| Commando | Doel |
|---|---|
init
|
Zet een nieuw project op in de huidige map |
add
|
Zet een nieuwe module op (een handler, een endpoint of een lib-module) |
login
|
Koppel het project aan een bot (slaat het token op) |
status
|
Toon wat lokaal is veranderd vs. de cloud |
diff
|
Toon de regel-voor-regel wijzigingen |
push
|
Implementeer gewijzigde modules en de build van de Mini App naar de cloud |
migrate
|
Pas schemawijzigingen toe op de database |
run
|
Voer een module uit op het platform zonder te implementeren |
fetch
|
Controleer wat er is geïmplementeerd, zonder lokale bestanden te wijzigen |
pull
|
Breng lokale bestanden op één lijn met de cloud |
reset
|
Verwijder lokale wijzigingen; herstel vanaf de cloud-status |
webhook
|
Inspecteer en synchroniseer de door het platform beheerde webhook |
completion
|
Print een shell-completion script (bash/zsh/fish) |
upgrade
|
Verplaats een pre-0.2.0 projectlayout naar tgcloud/ |
Authenticatie
Een project is gekoppeld aan één bot door zijn token, dat de vorm app<id>:<secret> heeft. Het app<id>-deel is publiek en mag worden geprint; het secret verschijnt nooit in logs of fouten.
Het token wordt in deze volgorde opgelost:
TGCLOUD_TOKENomgevingsvariabele — voor CI; nooit naar schijf geschreven..tgcloud/credentials— geschreven doornpx tgcloud login.- Geen van beide → een fout die je naar
npx tgcloud loginwijst.
De CLI vraagt nooit om een token tijdens een commando. Inloggen is altijd de expliciete login-stap. Als een opgeslagen token ongeldig wordt, verwijdert de CLI deze en vraagt je om opnieuw in te loggen.
init
npx tgcloud init
Zet een nieuw project op in de huidige map: de tgcloud/-map met schema.js, lib/, handlers/ en een starter-handler, plus tgcloud.jsonc, AGENTS.md, docs/ en de .tgcloud/-statusmap. Voer het ook uit in de root van een bestaand front-end project — het voegt de tgcloud/-map en de docs toe en overschrijft nooit je bestanden. Als het de front-end herkent, wijst het tgcloud.jsonc ook naar de build-map en voegt een deploy-script (bouwen, dan pushen) toe aan je package.json — alleen als je nog geen deploy-script hebt. Zie
Mini App front-end
.
init weigert een project binnen een ander project aan te maken. Het opnieuw uitvoeren in de eigen root van een project is prima en voegt alleen toe wat ontbreekt.
add
npx tgcloud add <doel>
Zet één nieuwe module op, gekoppeld en klaar om te bewerken — een handler, een endpoint of een lib/-module.
npx tgcloud add handlers/callback_query # een nieuwe update-handler
npx tgcloud add endpoints/getProfile # een nieuw endpoint voor de Mini App
npx tgcloud add lib/cart # een nieuwe gedeelde module
Merk op dat
addnooit een bestaand bestand overschrijft.
Het <doel> is het pad van de module (een afsluitende .js is optioneel). Voor handlers/ moet de naam een Telegram-updatetype zijn. Het platform adverteert de geldige set, dus een ongeldige naam wordt direct afgewezen. handlers/ en endpoints/ zijn plat, terwijl lib/ genest kan zijn (lib/payments/stripe).
De modulenaam is vereist. Alleen de map opgeven is een fout — maar een behulpzame: voor handlers/ somt het de updatetypen op die je nog niet hebt, zodat je er een kunt kopiëren.
$ npx tgcloud add handlers
Fout: Specificeer een naam, bijv. "handlers/callback_query". Beschikbare handlers/-typen: callback_query, inline_query, chat_member, …
<Tab>-completion biedt dezelfde set — ziecompletion.
Het gegenereerde bestand heeft een actieve export default, dus de handler is actief zodra je implementeert — er is niets uit te commentariëren. Implementeer de nieuwe module met
push
.
login
npx tgcloud login
Vraagt om je CLI-toegangstoken — van
@BotFather
→ je bot → Serverless → CLI Access → Access token, een ander token dan je
bot API-token
— valideert dit tegen het platform en slaat het op in .tgcloud/credentials.
loginis het enige commando dat om een token vraagt en vereist een interactieve terminal. Stel in CITGCLOUD_TOKENin.
status
npx tgcloud status
Toont per bestand wat er is veranderd tussen je werkmap en de geïmplementeerde kopie: gewijzigd, nieuw, verwijderd, ongewijzigd. Het werkt offline en vergelijkt met de status van de cloud zoals die was bij je laatste push, pull of fetch. Het waarschuwt ook voor losse .js-bestanden in tgcloud/ die niet zullen worden geïmplementeerd.
Voor een project met een Mini App toont status ook wat er sinds de laatste implementatie is veranderd in de build-map.
diff
npx tgcloud diff
Net als status, maar toont de werkelijke regel-voor-regel verschillen voor gewijzigde modules. Ook offline.
push
npx tgcloud push [bestanden...]
Implementeert je project naar de cloud in één atomaire batch.
Bij geen argumenten wordt het hele project geïmplementeerd en wordt de geïmplementeerde status exact gelijk aan je map — modules die je lokaal hebt verwijderd, worden in de cloud verwijderd.
Bij bestand- of mapargumenten (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), worden alleen de wijzigingen in die bestanden geïmplementeerd. Een gerichte push verwijdert nooit andere modules. Een doel kan worden geschreven als het bestandspad (tgcloud/handlers/message.js), het pad binnen tgcloud/ (handlers/message.js) of de modulenaam (handlers/message) — ze betekenen allemaal dezelfde module.
De enige optie is --force — om de concurrency-check over te slaan en te overschrijven wat er in de cloud staat. Gebruik dit alleen als je het zeker weet (zie
Gesynchroniseerd blijven
).
Als tgcloud.jsonc een statische map benoemt, implementeert een volledige push deze in dezelfde batch — alleen de gewijzigde bestanden worden geüpload — en print het adres van de site. Bouw eerst, want push voert nooit je build uit (npm run deploy doet beide). npx tgcloud push dist/ implementeert alleen de statische bestanden, en een push die alleen modules benoemt, laat ze ongemoeid.
Na een implementatie, als schema.js is gewijzigd en de database niet synchroon loopt, print push een samenvatting van de in behandeling zijnde wijzigingen en suggereert npx tgcloud migrate. Het past ze nooit zelf toe.
migrate
npx tgcloud migrate
Past je schemawijzigingen toe op de database. Het berekent het verschil tussen schema.js en de live database, en leidt je er vervolgens stap voor stap doorheen met een lopende [N/M]-teller:
- Een korte samenvatting van alle in behandeling zijnde wijzigingen.
- Veilige wijzigingen, samen toegepast in één stap op jouw bevestiging.
- Waarschuwingen (drops, trage operaties), één voor één, elk afzonderlijk bevestigd.
- Handmatige wijzigingen, getoond met een reden en voorgestelde actie, niet automatisch toegepast.
- Niet-gedocumenteerde objecten (in de database maar niet in je schema), getoond ter bewustwording.
Het eindigt met een samenvatting: toegepast, overgeslagen, wachtend op handmatige fix, niet in schema.
Zie Migraties voor het model.
Opties: --dry-run (print alles, pas niets toe), --safe (pas veilige wijzigingen automatisch toe, sla waarschuwingen over), --yes (pas veilige wijzigingen en elke waarschuwing automatisch toe, sla handmatig over — gebruik met zorg), --local (vergelijk tegen je lokale schema.js in plaats van de geïmplementeerde). Zonder vlag vereist migrate een terminal en geeft een fout in een niet-interactieve omgeving in plaats van te gokken.
run
npx tgcloud run <module> [args] [--ctx <json5>]
Voert een handler of een endpoint uit op het platform zonder te implementeren, met behulp van je huidige lokale bestanden. Dit is de snelle interne lus voor het testen van logica.
<module>— een kale naam (gezocht inhandlers/enendpoints/) of een pad zoalshandlers/message. Een kale naam die in beide mappen bestaat, moet met de map worden gespeld.[args]— de payload die aan de handler wordt doorgegeven, geschreven in JSON5 zodat je aanhalingstekens bij keys kunt overslaan. Het is het object van het updatetype dat je handler ontvangt (bijv. een Message voorhandlers/message), of deinputvan een endpoint, en het moet een object zijn — geen array of een kale string.--ctx <json5>— het contextobject van de module (het tweede argument), ook JSON5. Gebruik dit om te leveren wat je code leest uitctx— de ruwe update voor een handler,--ctx '{ update: { update_id: 1 } }', of de init-data voor een endpoint,--ctx '{ initData: { user: { id: 1 } } }'.runverifieert dit niet, omdat je je logica test, niet de checks van het platform.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
Het platform voert de module uit tegen de module-ruimte die is samengesteld uit je lokale project (dus lokaal gewijzigde lib/-code wordt ook gebruikt) en retourneert de retourwaarde, alles wat is gelogd met console.* en de verstreken tijd. Lees grote argumenten uit een bestand met npx tgcloud run handlers/message "$(cat message.json5)".
fetch
npx tgcloud fetch
Controleert wat er momenteel is geïmplementeerd, zonder je werkbestanden aan te raken. Hierna vergelijken status en diff tegen de nieuwste cloud-status. Handig om naar een conflict te kijken voordat je besluit hoe je het wilt oplossen.
pull
npx tgcloud pull
Brengt je lokale project op één lijn met de cloud door je werkbestanden bij te werken naar de geïmplementeerde status.
pull schrijft nooit de build-map van je Mini App.
reset
npx tgcloud reset
Verwijdert je lokale wijzigingen en herstelt de modules vanaf de laatst bekende cloud-status. Gebruik dit om een experiment weg te gooien. De build-map van de Mini App wordt niet aangeraakt.
webhook
npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]
Telegram levert updates aan je bot via een webhook, die het platform voor je beheert — je wijst het nooit met de hand ergens naar. npx tgcloud webhook toont de huidige status: de URL, de allowed_updates-lijst, hoeveel updates er in behandeling zijn, de laatste leveringsfout (indien aanwezig) en of deze synchroon loopt met je geïmplementeerde handlers.
“Synchroon” betekent dat de webhook naar het platform wijst en de allowed_updates overeenkomen met de handlers die je hebt geïmplementeerd — dus Telegram levert exact de updatetypen die je behandelt, en niets anders. Het implementeren van een nieuwe handler (of het verwijderen van een) kan de webhook uit sync laten lopen totdat deze wordt vernieuwd; npx tgcloud status vlagt dit ook.
npx tgcloud webhook sync lost dit op — het wijst de webhook opnieuw naar het platform en herbouwt allowed_updates op basis van je geïmplementeerde handlers. Voeg --drop-pending toe om updates te verwijderen die Telegram al in de wachtrij had gezet vóór de sync (anders worden ze geleverd zodra de webhook weer gezond is).
completion
Opmerking: tab-completion werkt alleen wanneer een kale
tgcloudop jePATHstaat — dus installeer het globaal (npm install -g @tgcloud/cli) of plaats het binaire bestand anders op jePATH. Het kan niet koppelen aannpx.
tgcloud completion <bash|zsh|fish>
Print een shell-completion script naar stdout. Schakel het eenmaal in, waarna <Tab> commando’s, vlaggen, modulemappen, de handler-updatetypen die je nog niet hebt en je lokale uitvoerbare modules aanvult.
# bash — vereist het bash-completion pakket:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc
# zsh — zorg ervoor dat `autoload -U compinit && compinit` draait in je ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc
# fish:
tgcloud completion fish > ~/.config/fish/completions/tgcloud.fish
Start daarna je shell opnieuw (of herlaad het bestand). Het uitvoeren van tgcloud completion zonder shell print deze instructies opnieuw.
upgrade
npx tgcloud upgrade [--dry-run]
Eenmalige migratie voor projecten gemaakt vóór CLI 0.2.0, die schema.js, lib/ en handlers/ in de root van het project hielden. Het verplaatst ze naar tgcloud/. Je koppeling met de bot blijft behouden en er hoeft niets opnieuw te worden geïmplementeerd. --dry-run print de geplande verplaatsingen zonder iets aan te raken. Andere commando’s draaien niet op de oude layout totdat je upgradet.
Gesynchroniseerd blijven
Elk project heeft een monotoon toenemende revisie in de cloud, die bij elke implementatie wordt opgehoogd. De CLI onthoudt de revisie waarmee het voor het laatst synchroniseerde en verzendt deze bij elke push. Als de cloud vooruit is gegaan — omdat een andere machine of teamgenoot heeft geïmplementeerd — wordt de push geweigerd in plaats van hun werk stilletjes te overschrijven, en biedt de CLI drie manieren voorwaarts:
npx tgcloud fetch # kijk wat er veranderd is in de cloud, en controleer opnieuw
npx tgcloud pull # breng het nieuwste in je werkbestanden
npx tgcloud push --force # overschrijf de cloud-status (gevaarlijk)
Deze optimistic-concurrency check is de reden waarom je een bot met een team kunt delen zonder een lockstep-implementatieproces. Commando’s eindigen non-zero bij een fout — een geweigerde implementatie, een mislukte migratie, een authenticatiefout, een module die gooide tijdens run — dus ze werken netjes in scripts en CI-pipelines.