# TMA.sh — Complete Documentation > TMA.sh is the deployment platform for Telegram Mini Apps. --- # TMA.sh > Deploy Telegram Mini Apps in seconds. Push code, get a live app. import { Card, CardGrid } from '@astrojs/starlight/components'; ## Why TMA.sh? Connect your GitHub repo, push to main, and your Telegram Mini App is live at `yourapp.tma.sh` in minutes. Validate Telegram users and get signed JWTs. Works with Supabase, Firebase, and any backend out of the box. Accept payments via TON Connect and Telegram Stars with just a few lines of code. Add a `server/api/index.ts` file and get an API deployed to the edge automatically. Use any framework or plain fetch handlers. ## Most Important Pieces - **Static app + edge APIs**: your frontend is served from `{project}.tma.sh`, and optional API routes from `{project}--api.tma.sh` (plus same-origin `/api/*` on project and preview hosts). - **Telegram-native auth**: `initData` validation + signed JWTs via `@tma.sh/sdk`, with JWKS-based verification for server routes. - **One deploy pipeline**: GitHub-based build/deploy flow for production and preview, with immutable deployments and instant route-based rollback. - **Built-in platform primitives**: per-project KV, optional managed D1, bot handlers, environment-scoped secrets, analytics, and campaigns. ## How it works ```bash # Install the CLI bun add -g @tma.sh/cli # Create a new project tma init my-app # Link this folder to a TMA.sh project cd my-app tma link # Start developing locally tma dev ``` Simple key-value storage scoped per project. No database setup needed for common use cases. Select one PR as staging and deploy it to `pr{number}--yourapp.tma.sh`, with optional preview-bot support. Roll back to any previous deployment instantly. No rebuilds, no downtime. Bring your own domain with automatic SSL certificates and global CDN. :::tip[Using an AI assistant?] This documentation is available as [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt) for LLMs and AI coding tools. ::: --- # Overview > What is TMA.sh and why use it TMA.sh is the deployment platform for Telegram Mini Apps. Push your code to GitHub, and TMA.sh automatically builds, deploys, and configures your Telegram bot. Your static SPA is served from a global CDN at `{project}.tma.sh`, ready for users in seconds. Think of it as **deployment-first infrastructure for Telegram Mini Apps** -- git-based deploys with auth, payments, storage, and bot routing built in. ## What you get Every project deployed to TMA.sh includes: - **Automatic builds** -- push to `main` and your app is live. No CI config required. - **Global CDN** -- static assets served from edge locations worldwide. - **Built-in auth** -- validate Telegram users and get signed JWTs from `initData`. Works with Supabase, Firebase, Turso, or any backend. - **Payments** -- accept TON and Telegram Stars with a few lines of code via `@tma.sh/sdk`. - **KV storage** -- simple key-value storage scoped per project. No database setup needed. - **Preview environments** -- select one pull request as your staging target and deploy it to `pr{number}--yourapp.tma.sh`, with optional preview-bot support. - **Instant rollback** -- revert to any previous deployment with zero downtime. - **Custom domains** -- bring your own domain with automatic SSL. - **Edge API routes** -- add a `server/api/index.ts` file and get an API deployed to `{project}--api.tma.sh`. Works with any framework that exports a standard `fetch` handler (Hono, itty-router, or plain Web API). ## Most important pieces - **Static SPA hosting** at `{project}.tma.sh` with immutable deployments and route-based rollback. - **Optional edge API routes** at `{project}--api.tma.sh`, plus same-origin `/api/*` proxying on project and preview hosts. - **Telegram auth and JWTs** through `@tma.sh/sdk` (`initData` validation + JWKS verification path). - **Project-scoped data** with built-in KV and optional managed D1 (Pro/Team). - **Preview environments** tied to one selected staging PR (`pr{number}--{project}.tma.sh`). ## Supported frameworks TMA.sh builds and deploys **static SPAs** only. Common setups: | Framework | Notes | |-----------|-------| | Vite | React, Vue, Svelte | | Astro | Static output mode | | Plain HTML | No build step required | The `tma init` command provides scaffold templates for **Vite React, Vite Vue, Vite Svelte, and Plain HTML**. Astro projects can be deployed by linking an existing Astro repository with `tma link` and configuring build settings in the dashboard if needed. SSR frameworks like Next.js, Nuxt, and SvelteKit are **not supported**. TMA.sh serves static files from a CDN -- if you need server-side rendering, those frameworks are not a fit. Use Vite with your preferred UI library instead. ## Prerequisites Before you start, make sure you have: - **Bun** -- the JavaScript runtime. Install from [bun.sh](https://bun.sh). - **A Telegram bot token** -- create one via [@BotFather](https://t.me/BotFather) in Telegram. - **A GitHub repository** -- TMA.sh deploys from GitHub. Public or private repos both work. ## Quick start Go from zero to a linked local project in a few commands: ```bash bun add -g @tma.sh/cli tma init my-app cd my-app tma link tma dev ``` Then connect your GitHub repository in the dashboard and push to your deploy branch (usually `main`) to trigger your first production deployment. See [Installation](/getting-started/installation/) for detailed setup instructions, or jump straight to [Your First Deploy](/getting-started/first-deploy/) for a step-by-step walkthrough. --- # Your First Deploy > Deploy a Telegram Mini App in under 5 minutes import { Steps } from '@astrojs/starlight/components'; This walkthrough takes you from an empty directory to a live Telegram Mini App. You will scaffold a project, test it locally, and deploy it to production. ## Create and develop locally 1. **Scaffold the project.** Run `tma init` and select the **Vite React** template: ```bash tma init my-first-app ``` 2. **Enter the project directory.** ```bash cd my-first-app ``` 3. **Link the directory to a TMA.sh project.** ```bash tma link ``` If needed, choose **+ Create new project** in the prompt. 4. **Edit your app.** Open `src/App.tsx` and replace the contents with a simple Mini App that greets the user: ```tsx import { useEffect, useState } from "react"; function App() { const [name, setName] = useState("there"); useEffect(() => { const webapp = window.Telegram?.WebApp; if (webapp) { webapp.ready(); const user = webapp.initDataUnsafe?.user; if (user?.first_name) { setName(user.first_name); } } }, []); return (

Hello, {name}!

Your first Telegram Mini App is running.

); } export default App; ``` 5. **Start the dev server.** The `tma dev` command starts a local Vite dev server with hot reload: ```bash tma dev ``` You will see output like: ``` Local: http://localhost:5173 ``` 6. **Test locally in the browser.** Open `http://localhost:5173` and verify hot module replacement works while you edit.
## Deploy to production Once you are happy with the app, deploy it to the world. 1. **Initialize a Git repo and push to GitHub.** ```bash git init git add . git commit -m "init" git remote add origin https://github.com/your-username/my-first-app.git git push -u origin main ``` 2. **Connect the repository.** Open the [TMA.sh dashboard](https://tma.sh/dashboard), navigate to your project, and connect your GitHub repository. This installs a GitHub webhook that triggers deployments on pushes to your configured deploy branch (default: `main`). 3. **Push a change to trigger a deploy.** Any push to `main` kicks off an automatic deployment. Check status in the [dashboard](https://tma.sh/dashboard) or via `tma logs`. 4. **Your app is live.** Once the deployment finishes, your Mini App is available at: ``` https://my-first-app.tma.sh ``` If a production bot is registered, its Web App URL is automatically updated to point at the production deployment. ## What happens during a deploy When you push to `main`, TMA.sh runs the following pipeline: 1. **GitHub webhook** -- TMA.sh receives the push event. 2. **Build container** -- a clean environment runs your project's configured install and build commands (defaults to `npm install` and `npm run build` for newly created dashboard projects unless you change them). 3. **Upload assets** -- the build output is uploaded to the global CDN. 4. **Update bot URL** -- if a production bot is registered, its Web App URL is pointed at the new deployment. 5. **Ready** -- your app is live and serving traffic. If you have a `server/api/index.ts` file, TMA.sh also bundles and deploys your API routes to `my-first-app--api.tma.sh`. :::note[Deployment limits] Deployments are validated before upload: static output is capped at `800 MB` total, code-like files at `10 MB` each, other static files at `25 MB` each, and API/bot bundles at `10 MB` each. See [Limits & Quotas](/reference/limits/). ::: ## Deployment statuses Each deployment moves through these stages: | Status | Meaning | |--------|---------| | **queued** | Push received, waiting for a build slot. | | **building** | Dependencies are being installed and the project is being built. | | **deploying** | Build output is being uploaded to the CDN and bot URL is being updated. | | **ready** | Deployment is live and serving traffic. | | **failed** | Something went wrong. Check the build logs with `tma logs`. | | **cleaned** | Deployment assets have been cleaned up (old deployments). | | **purged** | Deployment has been permanently removed. | ## Auto-deploy By default, every push to `main` triggers a deployment. You can toggle auto-deploy on or off in your project settings on the [dashboard](https://tma.sh/dashboard). When auto-deploy is off, use `tma deploy` to deploy manually. ## Next steps Your Mini App is live. From here, you can: - [Add API routes](/guides/api-routes/) to handle server-side logic. - [Set up authentication](/guides/authentication/) to identify Telegram users. - [Accept payments](/guides/payments/) via TON or Telegram Stars. - [Use KV storage](/guides/kv-storage/) for simple data persistence. - [Configure a custom domain](/guides/custom-domains/) for your app. --- # Installation > Install the TMA CLI and set up your account import { Steps } from '@astrojs/starlight/components'; The `tma` CLI is the primary tool for creating, developing, and deploying Telegram Mini Apps on TMA.sh. ## Install the CLI Install the CLI globally with Bun: ```bash bun add -g @tma.sh/cli ``` Verify the installation: ```bash tma --version ``` ## Log in to your account 1. Run the login command: ```bash tma login ``` This opens your browser and starts an OAuth device code flow. Authorize the CLI to connect it to your TMA.sh account. 2. Once authorized, credentials are stored locally. You only need to do this once per machine. ## Create a new project Use `tma init` to scaffold a new project from a template: ```bash tma init my-app ``` The interactive prompt walks you through: - **Template selection** -- pick a framework (Vite React, Vite Vue, Vite Svelte, or Plain HTML). - **API routes** -- optionally scaffold a `server/api/index.ts` file for edge API routes (Hono scaffolded by default, but any fetch-compatible framework works). - **Bot handlers** -- optionally set up bot command and message handlers. - **Dependency installation** -- automatically runs `bun install` (or `npm install` when Bun is unavailable). After init finishes, your project is ready to develop: ```bash cd my-app tma dev ``` `tma init` only scaffolds local files. To use authenticated project commands like `tma deploy`, `tma env`, `tma logs`, or `tma bot`, run `tma link` next to write the full project ID/org ID config. ## Link an existing project If you already have a frontend project and want to deploy it to TMA.sh, run `tma link` from the project root: ```bash cd my-existing-app tma link ``` This prompts you to select an org and project, then writes the `.tma/project.json` config file to connect your local directory to your TMA.sh account. If you select **+ Create new project** in the prompt, `tma link` creates the project first, then writes the same full config. ## Project configuration After `tma init` or `tma link`, a `.tma/project.json` file is created in your project root. `tma init` creates a partial config with only the project name: ```json { "projectName": "my-app" } ``` `tma link` creates the full config with all fields: ```json { "projectId": "11111111-2222-4333-8444-555555555555", "orgId": "66666666-7777-4888-8999-000000000000", "projectName": "my-app" } ``` This file identifies the project when running CLI commands. Commit `.tma/project.json`, but ignore local dev artifacts under `.tma/`: ```gitignore .tma/* !.tma/project.json ``` ## Set up your Telegram bot Every TMA.sh project is connected to a Telegram bot. If you do not have one yet, create it now. 1. Open Telegram and start a conversation with [@BotFather](https://t.me/BotFather). 2. Send `/newbot` and follow the prompts to choose a name and username. 3. Copy the bot token that BotFather gives you. You will need it when connecting your project. 4. Run `tma bot register` and paste the token when prompted: ```bash tma bot register ``` TMA.sh configures the bot's Web App URL automatically on each deployment. ## Next steps Your CLI is installed, your account is connected, and your project is ready. Head to [Your First Deploy](/getting-started/first-deploy/) to ship your first Telegram Mini App. --- # API Routes > Add server-side logic with edge API routes TMA.sh lets you add server-side logic to your Mini App by creating a `server/api/index.ts` file that exports a standard [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) handler. At build time, TMA.sh detects this file, bundles it with esbuild, and deploys it to Cloudflare Workers for Platforms. ## The contract Your entry file must export a default object with a `fetch` method that receives a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). This is the standard Cloudflare Workers interface -- any framework (or no framework) that produces this shape works: ```typescript // server/api/index.ts -- plain fetch handler, no framework needed export default { fetch(request: Request, env: Record) { const url = new URL(request.url); if (url.pathname === '/api/health') { return Response.json({ status: 'ok' }); } return Response.json({ error: 'Not found' }, { status: 404 }); }, }; ``` ## Using Hono (recommended) [Hono](https://hono.dev) is the recommended framework for API routes because the TMA SDK's `requireUser()` auth middleware is built as a Hono middleware. But it is not required -- you can use itty-router, worktop, or write plain fetch handlers. ```typescript import { Hono } from 'hono'; const app = new Hono(); app.get('/api/health', (c) => c.json({ status: 'ok' })); app.post('/api/items', async (c) => { const body = await c.req.json(); return c.json({ created: true, item: body }); }); export default app; ``` Deploy as usual with `tma deploy`. TMA.sh will detect the API entry point and bundle it automatically alongside your static assets. Routing options for deployed API routes: - Dedicated host: `{project}--api.tma.sh/*` - Dedicated preview host: `pr{number}--{project}--api.tma.sh/*` - Same-origin proxy on app hosts: `{project}.tma.sh/api/*` and `pr{number}--{project}.tma.sh/api/*` Custom domains currently proxy static assets only, not same-origin `/api/*`. ## Available bindings API routes run on Cloudflare Workers and have access to the following bindings via the `env` parameter: - **KV** -- a per-project KV namespace, provisioned automatically - **DB** -- a per-project D1 (SQLite) database, available when managed DB provisioning/migrations are applied for the project (Pro and Team plans only) - **Environment variables** -- secrets and configuration set via the dashboard or `tma env` With Hono, use TypeScript generics to get typed access: ```typescript import { Hono } from 'hono'; type Env = { Bindings: { KV: KVNamespace; MY_SECRET: string; DATABASE_URL: string; }; }; const app = new Hono(); app.get('/api/config', async (c) => { const cached = await c.env.KV.get('app-config'); if (cached) { return c.json(JSON.parse(cached)); } const config = { version: '1.0.0' }; await c.env.KV.put('app-config', JSON.stringify(config), { expirationTtl: 3600, }); return c.json(config); }); export default app; ``` With a plain fetch handler, access bindings directly from `env`: ```typescript export default { async fetch(request: Request, env: { KV: KVNamespace; MY_SECRET: string }) { const cached = await env.KV.get('app-config'); if (cached) { return Response.json(JSON.parse(cached)); } const config = { version: '1.0.0' }; await env.KV.put('app-config', JSON.stringify(config), { expirationTtl: 3600, }); return Response.json(config); }, }; ``` ## External databases For use cases that require features beyond what the managed D1 database provides, connect to an external database from your API routes: - **Supabase** -- use the Supabase client with your project's JWT (see [Authentication](/guides/authentication/)) - **Turso** -- use `@libsql/client` with a database URL and auth token stored as environment variables - **PlanetScale** -- use `@planetscale/database` with a connection string For relational data within the platform, see the [Managed Database guide](/guides/database/) to set up a per-project D1 database. ```typescript import { Hono } from 'hono'; import { createClient } from '@libsql/client'; type Env = { Bindings: { TURSO_URL: string; TURSO_AUTH_TOKEN: string; }; }; const app = new Hono(); app.get('/api/users', async (c) => { const db = createClient({ url: c.env.TURSO_URL, authToken: c.env.TURSO_AUTH_TOKEN, }); const result = await db.execute('SELECT * FROM users LIMIT 50'); return c.json(result.rows); }); export default app; ``` ## Middleware If you use Hono, its middleware works as expected. Common patterns include CORS, logging, and authentication: ```typescript import { Hono } from 'hono'; import { cors } from 'hono/cors'; const app = new Hono(); app.use('/api/*', cors({ origin: ['https://myapp.tma.sh'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE'], })); app.get('/api/data', (c) => c.json({ hello: 'world' })); export default app; ``` ## Limits | Resource | Limit | | ---------------- | ---------------- | | API route bundle | 10 MB | | Bot handler bundle (`bot/index.ts` or `.js`) | 10 MB | | Static output total | 800 MB | | Code-like static file (`.html`, `.css`, `.js`, `.mjs`, `.cjs`, `.wasm`, `.map`, `.webmanifest`) | 10 MB per file | | Other static file | 25 MB per file | | Runtime request limits | Inherited from Cloudflare Workers platform | Static output limits are validated during deployment before files are uploaded to R2. ## Route organization For larger APIs with Hono, use route grouping: ```typescript import { Hono } from 'hono'; import { users } from './routes/users'; import { items } from './routes/items'; const app = new Hono(); app.route('/api/users', users); app.route('/api/items', items); export default app; ``` All routes imported from subdirectories will be included in the esbuild bundle automatically. Only `server/api/index.ts` is used as the entry point. --- # Authentication > Authenticate Telegram users in your Mini App When Telegram opens your Mini App, it passes signed `initData` to the WebView. TMA.sh validates this data server-side and returns a JWT you can use to authenticate requests to your own backend or third-party services like Supabase. ## How it works 1. Telegram opens your Mini App and injects `initData` into the WebView 2. Your app sends `initData` to the TMA.sh auth endpoint via the SDK 3. TMA.sh validates the signature against your bot token 4. A signed JWT is returned containing the Telegram user's identity ## Client-side validation Use the SDK to validate `initData` and get a JWT: ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'your-project-id' }); const { user, jwt } = await tma.auth.validate( window.Telegram.WebApp.initData, 'your-project-id' ); console.log(user.telegramId); // 123456789 console.log(user.firstName); // "Alice" console.log(user.username); // "alice" ``` The returned `jwt` is a signed token you can attach to subsequent requests: ```typescript const response = await fetch('https://myapp--api.tma.sh/api/profile', { headers: { Authorization: `Bearer ${jwt}`, }, }); ``` ## JWT claims The JWT issued by TMA.sh contains the following claims: | Claim | Description | Example | | ------------- | ---------------------------------------------- | -------------------- | | `sub` | Unique subject identifier | `tg_123456789` | | `telegramId` | Telegram user ID | `123456789` | | `firstName` | User's first name | `Alice` | | `lastName` | User's last name (may be empty) | `Smith` | | `username` | Telegram username (may be empty) | `alice` | | `projectId` | Your TMA.sh project ID | `11111111-2222-4333-8444-555555555555` | | `iat` | Issued at (Unix timestamp) | `1700000000` | | `exp` | Expires at (24 hours after issuance) | `1700086400` | ## Server-side middleware Protect your API routes with the `requireUser()` middleware: ```typescript import { Hono } from 'hono'; import { requireUser } from '@tma.sh/sdk/server'; const app = new Hono(); app.use('/api/protected/*', requireUser()); app.get('/api/protected/profile', (c) => { const user = c.get('user'); return c.json({ telegramId: user.telegramId, username: user.username, }); }); export default app; ``` The middleware verifies the JWT signature, checks expiration, and attaches the decoded user to the request context. Unauthorized requests receive a `401` response. ## Supabase integration You can forward the TMA JWT to Supabase as a Bearer token from your Mini App: ```typescript import { createClient } from '@supabase/supabase-js'; import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'your-project-id' }); const { jwt } = await tma.auth.validate( window.Telegram.WebApp.initData, 'your-project-id' ); const supabase = createClient( 'https://your-project.supabase.co', 'your-anon-key', { global: { headers: { Authorization: `Bearer ${jwt}` }, }, } ); // Supabase queries now run with the forwarded JWT const { data } = await supabase .from('profiles') .select('*') .eq('telegram_id', 'tg_123456789'); ``` To enforce RLS with TMA JWTs, configure Supabase auth verification to trust TMA-issued tokens (for example via JWKS/external JWT verification). The public key set is available at `https://api.tma.sh/.well-known/jwks.json`. ## JWKS endpoint For custom backend verification, TMA.sh exposes a JWKS (JSON Web Key Set) endpoint: ``` https://api.tma.sh/.well-known/jwks.json ``` This is a global endpoint (not per-project). Use it to verify JWTs in any language or framework that supports JWKS: ```typescript import { createRemoteJWKSet, jwtVerify } from 'jose'; const JWKS = createRemoteJWKSet( new URL('https://api.tma.sh/.well-known/jwks.json') ); const { payload } = await jwtVerify(token, JWKS); // payload.sub === 'tg_123456789' ``` ## Framework helpers ### React ```tsx import { TMAProvider, useTelegramAuth } from '@tma.sh/sdk/react'; function App() { return ( ); } function Profile() { const { user, jwt, isLoading, error } = useTelegramAuth(); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return
Welcome, {user.firstName}!
; } ``` ### Svelte ```svelte {#if $auth.isLoading}

Loading...

{:else if $auth.user}

Welcome, {$auth.user.firstName}!

{/if} ``` ## Security considerations - JWTs expire after 24 hours. Re-validate `initData` to get a fresh token. - Never expose your bot token or JWT signing secret in client-side code. - Always verify JWTs server-side before trusting user identity. - Use HTTPS for all API requests (TMA.sh enforces this by default). --- # Custom Domains > Use your own domain with TMA.sh By default, your Mini App is served at `{project}.tma.sh`. You can add a custom domain so users access your app at your own address, like `app.yourdomain.com`. ## Adding a custom domain ### Step 1: Add the domain in the dashboard Go to your project's **Settings > Domains** page and enter your domain (e.g., `app.yourdomain.com`). ### Step 2: Configure DNS For subdomain hosts (e.g., `app.yourdomain.com`), add a `CNAME` record pointing to your project host: | Type | Name | Value | TTL | | ------- | ------ | --------- | ---- | | `CNAME` | `app` | `{project}.tma.sh` | Auto | For an apex/root domain (e.g., `yourdomain.com`), many DNS providers require `ALIAS`/`ANAME` flattening. Point apex to `tma.sh`. ### Step 3: Verify in the dashboard After DNS is in place, click **Verify** in **Settings > Domains**. Verification usually completes within minutes, but DNS propagation can take longer depending on your provider. ### Step 4: SSL is provisioned automatically Once the domain is verified, TMA.sh provisions an SSL certificate via Cloudflare. HTTPS is enforced by default -- no configuration needed. ## Domain statuses | Status | Meaning | | ----------- | ------------------------------------------------------ | | **Pending** | DNS record not yet detected. Waiting for propagation. | | **Active** | Domain verified and serving traffic with SSL. | | **Failed** | Verification failed. Check your DNS configuration. | You can check the current status on the **Settings > Domains** page in the dashboard. ## Multiple domains You can add multiple custom domains to a single project. All domains serve the same deployment. This is useful for: - Regional domains (e.g., `app.yourdomain.com` and `app.yourdomain.de`) - Migrating from an old domain to a new one - Vanity URLs ## API route domains Custom domains apply to your static SPA. API routes remain accessible at `{project}--api.tma.sh/*`. If you need a custom domain for your API, contact support. ## Removing a domain To remove a custom domain, go to your project's **Settings > Domains** page in the dashboard and delete the domain entry. Traffic to that domain will stop resolving. Remember to clean up the DNS record at your provider as well. Custom domain management is dashboard-only -- there are no CLI commands for adding, listing, or removing domains. --- # Managed Database > Per-project D1 (SQLite) database with migration-driven provisioning Every TMA.sh project on a Pro or Team plan can have its own managed D1 database -- a full SQLite database running on Cloudflare's edge network. No connection strings, no external services. Database provisioning happens when migrations are included in deploy processing. ## How it works TMA.sh uses a **developer-owned schema** model: 1. You define your database schema locally using Drizzle ORM (or write SQL by hand). 2. You generate SQL migration files into `db/migrations/`. 3. You commit and deploy. When migration SQL is included in build output, TMA.sh provisions a D1 database on first deploy and applies pending migrations. The database is lazily provisioned -- it only gets created when your deploy includes migration SQL statements. ## Setting up your schema Install Drizzle ORM and drizzle-kit as dev dependencies: ```bash bun add drizzle-orm bun add -d drizzle-kit ``` Create a `drizzle.config.ts` at the root of your project: ```typescript import { defineConfig } from 'drizzle-kit'; export default defineConfig({ dialect: 'sqlite', schema: './db/schema.ts', out: './db/migrations', }); ``` Define your schema in `db/schema.ts`: ```typescript import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), telegramId: integer('telegram_id').notNull().unique(), username: text('username'), score: integer('score').default(0), createdAt: integer('created_at', { mode: 'timestamp' }) .notNull() .$defaultFn(() => new Date()), }); export const items = sqliteTable('items', { id: integer('id').primaryKey({ autoIncrement: true }), userId: integer('user_id') .notNull() .references(() => users.id), name: text('name').notNull(), rarity: text('rarity', { enum: ['common', 'rare', 'legendary'] }).notNull(), }); ``` ## Generating migrations Run drizzle-kit to generate SQL migration files: ```bash npx drizzle-kit generate ``` This creates timestamped `.sql` files in `db/migrations/`: ``` db/ schema.ts migrations/ 0000_initial.sql meta/ _journal.json 0000_snapshot.json ``` Each `.sql` file contains the DDL statements for that migration step. Commit these files to your repository. :::note Never edit generated migration files by hand. If you need to change your schema, update `db/schema.ts` and run `npx drizzle-kit generate` again to produce a new migration. ::: ## Deploying Push your code as usual. During deploy processing, TMA.sh applies migrations when migration SQL is present in build output: 1. **First deploy with migrations**: Provisions a new D1 database for the project and applies all migration statements. 2. **Subsequent deploys**: Applies only new (pending) migrations. Already-applied migrations are skipped. If no migration SQL is included in deploy processing, the migration step is skipped. ## Accessing the database in API routes The database is available as the `DB` binding in your API routes. Use it with Drizzle ORM for typed queries or with the raw D1 API for direct SQL. ### With Drizzle ORM (recommended) ```typescript import { Hono } from 'hono'; import { drizzle } from 'drizzle-orm/d1'; import { eq } from 'drizzle-orm'; import * as schema from '../../db/schema'; type Env = { Bindings: { DB: D1Database; }; }; const app = new Hono(); app.get('/api/users/:id', async (c) => { const db = drizzle(c.env.DB, { schema }); const userId = Number(c.req.param('id')); const user = await db .select() .from(schema.users) .where(eq(schema.users.telegramId, userId)) .get(); if (!user) { return c.json({ error: 'User not found' }, 404); } return c.json(user); }); app.post('/api/users', async (c) => { const db = drizzle(c.env.DB, { schema }); const { telegramId, username } = await c.req.json(); const user = await db .insert(schema.users) .values({ telegramId, username }) .returning() .get(); return c.json(user, 201); }); export default app; ``` ### With the raw D1 API ```typescript app.get('/api/leaderboard', async (c) => { const result = await c.env.DB.prepare( 'SELECT telegram_id, username, score FROM users ORDER BY score DESC LIMIT 50' ).all(); return c.json(result.results); }); app.get('/api/users/:id/items', async (c) => { const userId = c.req.param('id'); const result = await c.env.DB.prepare( 'SELECT * FROM items WHERE user_id = ?' ) .bind(userId) .all(); return c.json(result.results); }); ``` Always use parameterized queries (`.bind()`) to prevent SQL injection. ## Tier requirements Managed databases are not available on the Free plan. | Plan | Database storage | |------|-----------------| | Free | Not available | | Pro | 500 MB | | Team | 2 GB | Storage quota is checked before each migration run at deploy time. If applying a migration would exceed your plan's storage limit, the deploy will fail with a quota error. Upgrade your plan or reduce stored data to continue. ## Limitations - **One database per project** -- each project gets a single D1 database. - **Migrations only at deploy time** -- you cannot run ad-hoc migrations outside of a deploy. - **No direct access** -- there is no connection string or external endpoint. The database is only accessible via the `DB` binding in API routes. - **SQLite semantics** -- D1 is SQLite. Some PostgreSQL or MySQL features (stored procedures, advanced JSON operators, certain window functions) are not available. - **Max query execution time** -- 30 seconds per query. - **D1 per-database limit** -- 10 GB hard ceiling regardless of plan. ## Common use cases - **User profiles** -- store structured user data with relational queries - **Game state** -- leaderboards, inventory, achievements with proper indexing - **Orders and transactions** -- track purchases and payment history - **Content management** -- store app content that needs filtering, sorting, and pagination ## When to use something else For use cases that require features beyond what D1/SQLite provides, connect to an external database from your API routes: - **Real-time subscriptions** -- Supabase (PostgreSQL with real-time) - **Global replication** -- Turso (libSQL with edge replicas) - **Full-text search** -- a dedicated search service like Meilisearch See [API Routes](/guides/api-routes/) for examples of connecting to external databases. --- # Environment Variables > Manage secrets and configuration for your deployments Environment variables let you store secrets and configuration outside your codebase. They are encrypted at rest, decrypted at deploy time, and injected into your API routes as environment variables. ## Setting variables ### Via the CLI ```bash # Set a variable tma env set DATABASE_URL=postgres://... # Set another variable tma env set ANALYTICS_KEY=ak_123 # List all variables tma env list # Remove a variable tma env remove DATABASE_URL # Pull variable names to a local .env.local file (values are redacted) tma env pull ``` The CLI sets variables for the production environment. Environment scoping beyond production (preview, development) is managed through the dashboard. ### Via the dashboard Navigate to your project's **Settings > Environment Variables** page. Add, edit, or remove variables from the web interface. Changes take effect on the next deployment. ## Environment scoping Every secret in TMA.sh is scoped to a specific environment: | Scope | Applied to | | -------------- | ------------------------------------------- | | **Production** | Production deployments | | **Preview** | Preview deployments | | **Development**| Reserved for development-scoped workflows | Environment scoping is managed through the dashboard at **Settings > Environment Variables**. The CLI (`tma env set`) always writes `production` secrets. Deployment builds currently load `production` or `preview` scoped secrets based on deployment type. ## Accessing variables in API routes Environment variables are available via the `env` parameter in your API routes. With Hono, access them on `c.env`; with plain fetch handlers, they are the second argument to `fetch(request, env)`: ```typescript import { Hono } from 'hono'; type Env = { Bindings: { DATABASE_URL: string; STRIPE_SECRET_KEY: string; RESEND_API_KEY: string; }; }; const app = new Hono(); app.post('/api/send-email', async (c) => { const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { Authorization: `Bearer ${c.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: 'noreply@myapp.tma.sh', to: 'user@example.com', subject: 'Hello', text: 'Welcome to the app!', }), }); return c.json({ sent: res.ok }); }); export default app; ``` ## Local development `tma dev` does not automatically pull dashboard secrets into local worker bindings. For local frontend code, Vite's standard `.env` loading still applies: ```bash # .env (or .env.local) DATABASE_URL=postgres://localhost:5432/myapp STRIPE_SECRET_KEY=sk_test_... ``` For worker-side local values, provide explicit test values in your local code/test setup. ## Common use cases | Variable | Purpose | | --------------------- | ------------------------------------ | | `DATABASE_URL` | External database connection string | | `BOT_TOKEN` | Telegram bot token | | `STRIPE_SECRET_KEY` | Payment processor credentials | | `RESEND_API_KEY` | Email service API key | | `SENTRY_DSN` | Error tracking | ## Security - Variables are **encrypted at rest** and only decrypted during deployment. - They are **never included** in your static SPA bundle -- only API routes have access. - Avoid logging secret values from your own build scripts or runtime code. - Use the dashboard or CLI to **rotate secrets** without changing code -- trigger a redeploy to pick up the new value. - Add `.env` to your `.gitignore` to prevent accidentally committing local secrets. --- # KV Storage > Simple key-value storage for your Mini App Every TMA.sh project includes a dedicated KV (key-value) namespace backed by Cloudflare KV. No setup or configuration required -- it is provisioned automatically when you create a project. ## Client-side API The SDK provides a simple interface for reading and writing KV data from your Mini App: ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'your-project-id' }); // Store a value await tma.kv.set('user:preferences', { theme: 'dark', language: 'en', }); // Retrieve a value const prefs = await tma.kv.get('user:preferences'); // { theme: 'dark', language: 'en' } // Remove a value await tma.kv.remove('user:preferences'); // List keys by prefix const result = await tma.kv.list('user:123:'); // result.keys = [{ name: 'user:123:preferences' }, { name: 'user:123:scores' }] // result.list_complete = true const keyNames = result.keys.map(k => k.name); ``` Values are automatically serialized to JSON. The client-side API routes requests through your project's API endpoint, so authentication is enforced -- users can only access KV data scoped to your project. ## Server-side API In API routes, access the raw KV namespace binding directly: ```typescript import { Hono } from 'hono'; type Env = { Bindings: { KV: KVNamespace; }; }; const app = new Hono(); app.get('/api/leaderboard', async (c) => { const leaderboard = await c.env.KV.get('leaderboard', 'json'); return c.json(leaderboard ?? []); }); app.post('/api/leaderboard/score', async (c) => { const { userId, score } = await c.req.json(); const leaderboard = await c.env.KV.get('leaderboard', 'json') ?? []; const updated = [...leaderboard, { userId, score }] .sort((a, b) => b.score - a.score) .slice(0, 100); await c.env.KV.put('leaderboard', JSON.stringify(updated)); return c.json({ rank: updated.findIndex((e) => e.userId === userId) + 1 }); }); export default app; ``` You can also use the SDK's server helper for a higher-level API: ```typescript import { createKV } from '@tma.sh/sdk/server'; app.get('/api/config', async (c) => { const kv = createKV(c.env.KV); const config = await kv.get('app-config'); return c.json(config); }); app.get('/api/keys', async (c) => { const kv = createKV(c.env.KV); const keys = await kv.list('user:'); // string[] return c.json({ keys }); }); ``` ## KV with expiration Set a TTL (time-to-live) on keys for automatic expiration. TTL is available server-side only -- the client-side `tma.kv.set()` method signature is `set(key: string, value: unknown)` with no TTL option. ```typescript // Server-side (raw KV binding): expires in 1 hour await c.env.KV.put('session:abc', JSON.stringify(data), { expirationTtl: 3600, }); // Server-side (createKV helper): also supports TTL const kv = createKV(c.env.KV); await kv.set('cache:feed', feedData, 3600); ``` ## Limits | Resource | Limit | | ---------------- | ---------------------------- | | Max value size | 128 KB (131,072 bytes) | | Max key length | 512 bytes | | Consistency | Eventually consistent (~60s) | | Reads | Unlimited | | Writes | 1,000 per second per key | KV is eventually consistent, meaning a write in one region may take up to 60 seconds to propagate globally. For most Mini App use cases this is not noticeable. ## Common use cases - **User preferences** -- theme, language, notification settings - **Feature flags** -- toggle features without redeploying - **Leaderboards** -- store and sort scores for game-style apps - **Session data** -- temporary state that expires automatically - **Cached API responses** -- reduce external API calls with TTL-based caching ## When to use something else KV storage is not a database. It does not support queries, indexes, transactions, or relational data. For those needs, use the [managed D1 database](/guides/database/) (Pro and Team plans) or connect to an external database from your API routes: - **Managed D1** -- per-project SQLite database with Drizzle ORM support - **Supabase** -- PostgreSQL with auth and real-time subscriptions - **Turso** -- SQLite at the edge with libSQL - **PlanetScale** -- serverless MySQL See [API Routes](/guides/api-routes/) for examples of connecting to external databases. --- # Local Development > Develop and test your Mini App locally The `tma dev` command starts a local development environment for your Mini App. It runs your SPA and, when present, local API and bot workers. ## What `tma dev` starts Running `tma dev` in your project directory spins up the following: | Service | Port | Description | | ------- | ---- | ----------- | | Vite dev server | `5173` | Your SPA with hot module replacement (always) | | API worker | `8787` | Miniflare worker for `server/api/index.ts|js` (if present) | | Bot worker | `8788` | Miniflare worker for `bot/index.ts|js` (if present) | ```bash tma dev ``` ``` App http://localhost:5173 API http://localhost:5173/api (proxied, when API worker exists) Bot http://localhost:5173/bot (proxied, when bot worker exists) Press Ctrl+C to stop. ``` ## How it works 1. **Vite dev server** starts on port 5173 with HMR for instant feedback during development. 2. **API routes**: if `server/api/index.ts` or `server/api/index.js` exists, TMA.sh watches and bundles it with esbuild, then runs it in Miniflare on port 8787. The Vite dev server proxies `/api/*` requests to this worker. 3. **Bot handlers**: if `bot/index.ts` or `bot/index.js` exists, TMA.sh watches and bundles it with esbuild, then runs it in Miniflare on port 8788. The Vite dev server proxies `/bot/*` requests to this worker. ## Bot setup Use `tma bot register` to register production, preview, or development bots: ```bash tma bot register ``` `tma dev` itself runs fully local workers and does not mutate your remote bot configuration. ## Local KV persistence KV data written during local development is persisted to disk under `.tma/` in your project directory, so state survives restarts of `tma dev`. ``` my-app/ .tma/ kv-data/ # Local API worker KV persistence bot-kv-data/ # Local bot worker KV persistence (if bot worker runs) project.json # Project configuration server/ api/ index.ts src/ ... ``` To reset local state, delete `.tma/kv-data/` and/or `.tma/bot-kv-data/`. ## Environment variables `tma dev` does not automatically pull dashboard secrets into local Miniflare bindings. For frontend code, Vite's normal `.env` loading still applies. For local worker bindings, provide local values explicitly in your code or test helpers. See [Environment Variables](/guides/environment-variables/) for more details. ## Debugging API routes API route logs are printed to the same terminal as the dev server. Use `console.log` in your API handlers during development: ```typescript app.get('/api/debug', async (c) => { const data = await c.env.KV.get('key', 'json'); console.log('KV data:', data); return c.json(data); }); ``` Miniflare prints logs alongside the Vite dev server output for a unified view of both client and server activity. --- # Payments > Accept payments with TON Connect and Telegram Stars TMA.sh supports two payment methods for Telegram Mini Apps: **TON Connect** for cryptocurrency payments and **Telegram Stars** for in-app purchases. ## Telegram Stars Telegram Stars is Telegram's built-in payment system. Users pay with Stars (purchased through Telegram), and you receive payouts via Fragment. ### Quick start (recommended) The SDK's `payments` namespace handles everything -- invoice creation, payment sheet, and result tracking. No bot token needed client-side: ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'my-project' }); // Authenticate first const { user } = await tma.auth.validate( window.Telegram.WebApp.initData, 'my-project' ); // One-liner payment const result = await tma.payments.stars.pay({ title: 'Premium Subscription', description: 'Access premium features for 30 days', payload: JSON.stringify({ userId: user.telegramId, plan: 'premium' }), prices: [{ label: 'Premium (30 days)', amount: 100 }], }); if (result.status === 'paid') { // Payment successful - update UI } ``` The `pay()` method creates the invoice via the platform (which uses your project's stored bot token), opens the native Telegram payment sheet, and resolves with the result. ### Advanced: server-side invoice creation If you need more control, use the typed `createInvoiceLink()` method on `TelegramApiClient` in your own API routes: ```typescript import { createTelegramApiClient } from '@tma.sh/sdk/server'; app.post('/api/create-invoice', async (c) => { const telegram = createTelegramApiClient(c.env.BOT_TOKEN); const invoiceUrl = await telegram.createInvoiceLink({ title: 'Premium Subscription', description: 'Access premium features for 30 days', payload: JSON.stringify({ plan: 'premium' }), currency: 'XTR', prices: [{ label: 'Premium (30 days)', amount: 100 }], }); return c.json({ invoiceUrl }); }); ``` ### Handle payment webhooks After a successful Stars payment, Telegram sends a `pre_checkout_query` (which you must answer within 10 seconds) and then a `successful_payment` update. Use the typed `answerPreCheckoutQuery()` method in your bot handler: ```typescript import { defineBot } from '@tma.sh/sdk/bot'; export default defineBot({ onPreCheckoutQuery: async (ctx) => { // Validate the order and confirm await ctx.answerPreCheckoutQuery(true); }, }); ``` TMA.sh automatically tracks `stars_payment` analytics events with campaign attribution when payments complete through the bot webhook. Revenue is attributed to the user's first-touch campaign `startParam` and shows up in the dashboard's campaign metrics with ARPU. ## TON Connect TON Connect enables wallet-based cryptocurrency payments directly in the Mini App. TON payments are handled client-side using the `@tonconnect/ui` package directly -- they are not part of the TMA.sh SDK. ### Install the package ```bash npm install @tonconnect/ui ``` ### Connect a wallet and send a transaction ```typescript import { TonConnectUI } from '@tonconnect/ui'; const tonConnectUI = new TonConnectUI({ manifestUrl: 'https://myapp.tma.sh/tonconnect-manifest.json', }); // Connect the user's wallet await tonConnectUI.connectWallet(); // Send a transaction const transaction = { validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes messages: [ { address: 'UQBx...your-wallet-address', amount: '1500000000', // 1.5 TON in nanotons }, ], }; const result = await tonConnectUI.sendTransaction(transaction); ``` The `amount` is specified in nanotons (1 TON = 1,000,000,000 nanotons). You need to host a `tonconnect-manifest.json` file at your app's root describing your application. ### Verify on the server For order fulfillment, verify the transaction server-side by querying the TON blockchain directly using a TON API provider (such as TON Center or TON API). This is outside the scope of TMA.sh. ## Choosing a payment method | Feature | TON Connect | Telegram Stars | | ---------------- | ---------------------------- | ----------------------------- | | Currency | TON (cryptocurrency) | Stars (in-app currency) | | User experience | Wallet approval popup | Native Telegram payment sheet | | Server required | Optional (for verification) | Yes (invoice creation) | | Payout | Direct to your TON wallet | Via Fragment | | Fees | Network gas fees only | Telegram's standard cut | | Best for | Crypto-native users, NFTs | Digital goods, subscriptions | ## Testing payments During local development (`tma dev`), both payment methods work with test networks: - **TON Connect**: Use the testnet flag in your TON Connect configuration to connect to TON testnet wallets - **Telegram Stars**: Use Telegram's test environment with test Stars (available via `@BotFather` in test mode) See [Local Development](/guides/local-development/) for setup details. --- # Build Pipeline > How TMA.sh builds and deploys your app TMA.sh builds from your linked GitHub repository and deploys immutable outputs. This page describes the exact flow used by the platform worker and background build worker. ## Build configuration source Build settings come from your project's `buildConfig`: ```json { "installCommand": "npm install", "buildCommand": "npm run build", "outputDir": "dist" } ``` For newly created dashboard projects, these are the default values unless you change them in project settings. ## Build steps The deployment pipeline is: ``` 1. Trigger deployment (GitHub webhook, dashboard, or CLI) 2. Create deployment record (status: queued) 3. Enqueue build job 4. Build worker claims deployment (status: building) 5. Build container: a. Clone repo at branch/commit b. Run install command at repo root c. Run build command at repo root d. Validate output (`index.html` + size limits) e. Upload static files to R2 f. Bundle optional API routes / bot handlers 6. Background deploy steps: a. Persist build metadata and build log b. Provision/update KV and optional D1 c. Deploy optional API/bot workers d. Activate production or preview routing 7. Deployment status becomes ready ``` If any step fails, deployment status becomes `failed` and existing production routing is unchanged. ## Timeouts - **Install/build command timeout**: 5 minutes per command inside the build container - **Container request timeout**: 10 minutes end-to-end for the background builder call ## Deployment size limits (enforced at build time) Before uploading files to R2, TMA.sh validates your build output: | Artifact | Limit | |----------|-------| | Static output (total) | 800 MB | | Code-like static file (`.html`, `.css`, `.js`, `.mjs`, `.cjs`, `.wasm`, `.map`, `.webmanifest`) | 10 MB per file | | Other static file (images, fonts, media, docs, etc.) | 25 MB per file | | API route bundle (`server/api/index.ts` or `.js`) | 10 MB | | Bot handler bundle (`bot/index.ts` or `.js`) | 10 MB | If any limit is exceeded, the deployment fails during validation with a clear error in build logs. ## Deployment status flow A deployment moves through a fixed set of statuses: ``` queued → building → deploying → ready ↘ failed ready/failed (older retained window) → cleaned → purged ``` - **queued** -- Deployment record exists, waiting for a build container. - **building** -- Build container is running: cloning/installing/building. - **deploying** -- Build succeeded. Assets are being uploaded and routing is being updated. - **ready** -- Deployment is live and serving traffic. - **failed** -- Something went wrong. Build logs contain the error details. - **cleaned** -- Marked as cleaned by retention or preview cleanup logic. - **purged** -- Final cleanup state after cron processes cleaned deployments. ## Server routes TMA.sh is primarily a static hosting platform, but it supports lightweight server-side API routes for cases where your Mini App needs backend logic. If your project contains a `server/api/index.ts` or `server/api/index.js` file, TMA.sh detects it during the build step, bundles it with esbuild, and deploys it to Cloudflare Workers for Platforms. Your API routes are then accessible at: ``` https://{project}.tma.sh/api/* (same-origin proxy) https://pr{number}--{project}.tma.sh/api/* (same-origin preview proxy) https://{project}--api.tma.sh/* (dedicated API host) https://pr{number}--{project}--api.tma.sh/* (dedicated preview API host) ``` Server routes have access to KV storage, environment variables defined in your project's secrets, and the managed D1 database (if provisioned). The only requirement is that your entry file exports a default object with a standard `fetch` handler. Any framework that produces this shape works -- Hono, itty-router, or plain Web API: ```typescript // server/api/index.ts -- using Hono (recommended) import { Hono } from "hono" const app = new Hono() app.get("/api/health", (c) => { return c.json({ status: "ok" }) }) export default app ``` ```typescript // server/api/index.ts -- plain fetch handler, no framework export default { fetch(request: Request, env: Record) { return Response.json({ status: "ok" }) }, } ``` ## Database migrations When the build output includes database migration SQL statements, TMA.sh provisions/migrates a D1 database during the deploy step: 1. **First deploy with migrations** -- A new D1 database is created for the project and migrations are applied in order. 2. **Subsequent deploys** -- Only new (pending) migrations are applied. Already-applied migrations are tracked and skipped. 3. **No migration payload in build output** -- The migration step is skipped and no database is provisioned. Migration application happens after the build succeeds and before routing tables are updated. If a migration fails, the deployment is marked as `failed` and the previous production deployment continues serving traffic. Storage quota is checked against your plan's limit before migrations are applied. A managed database requires a Pro or Team plan. See the [Managed Database guide](/guides/database/) for setup instructions. ## Bot handlers If your project contains a `bot/index.ts` or `bot/index.js` file, TMA.sh bundles it separately and deploys it as a bot webhook handler. This allows your Telegram bot to respond to commands and messages alongside serving the Mini App. ## Build logs Build logs are streamed in near real time through KV while a build is running, then persisted to R2 for long-term retrieval after completion. Logs are accessible from the dashboard and CLI (`tma logs`). Typical log content includes: - Dependency installation output - Build command output (stdout and stderr) - Asset upload summary (file count, total size) - Route activation/update messages ## Build environment Builds run in isolated Cloudflare Containers with: - **Runtime**: Bun (latest stable) - **Memory**: Sufficient for typical frontend builds - **Timeout**: Builds that exceed the time limit are terminated and marked as failed - **Network**: Outbound access for installing dependencies from npm registries - **Isolation**: Each build runs in its own container with no access to other projects --- # Hosting & CDN > How your Mini App is served globally Once your Mini App is built, TMA.sh serves it from Cloudflare's global network. This page explains how requests are routed, how caching works, and how rollbacks are handled. ## Asset serving Every request to a TMA.sh-hosted app follows the same path: ``` Browser → Cloudflare Worker → KV lookup → R2 fetch → Response ``` :::note[Build-time asset limits] Before files are uploaded to R2, TMA.sh validates deployment artifacts: static output max `800 MB` total, code-like files max `10 MB` each, other static files max `25 MB` each. API route and bot handler bundles are also capped at `10 MB` each. See [Limits & Quotas](/reference/limits/). ::: ### Subdomain routing Each project gets a subdomain at `{project}.tma.sh`. When a request arrives: 1. The Worker extracts the subdomain from the hostname. 2. It performs a KV lookup for the key `route:{subdomain}`. 3. The value contains routing metadata (including deployment ID and project/org IDs). 4. The Worker fetches the requested file from R2 and returns it. ``` myapp.tma.sh/index.html → KV get "route:myapp" → { deploymentId: "...", ... } → R2 get "deployments/{deploymentId}/index.html" → 200 OK ``` ### Custom domains Custom domains work the same way, with a different KV key pattern: ``` app.example.com/index.html → KV get "route:custom:app.example.com" → { deploymentId: "...", ... } → R2 get "deployments/{deploymentId}/index.html" → 200 OK ``` For subdomains, use CNAME to `{project}.tma.sh`. For apex/root domains, use ALIAS/ANAME flattening to `tma.sh`. TLS certificates are provisioned automatically through Cloudflare after verification. ## Cache headers TMA.sh sets cache headers based on the type of file being served: | File type | Cache-Control | Rationale | |-----------|---------------|-----------| | HTML files | `no-cache, must-revalidate` | Always serve the latest version | | Hashed assets (e.g., `app.a1b2c3d4.js`) | `public, max-age=31536000, immutable` | Content-addressed, safe to cache indefinitely | | Other static files | `public, max-age=86400` | Cache for 24 hours, reasonable freshness | This strategy ensures that users always load the latest HTML (which references the latest hashed assets), while hashed JavaScript, CSS, and image files are cached aggressively at the edge and in browsers. ### How it works in practice When you deploy a new version: 1. New hashed asset files are uploaded to R2 (e.g., `app.a1b2c3d4.js`). 2. A new `index.html` is uploaded that references these new files. 3. The KV routing pointer is updated to the new deployment. 4. The next request for `index.html` gets the new version (no-cache), which loads the new hashed assets. 5. Old hashed assets remain in R2 until the deployment is cleaned up, so in-flight requests are not broken. ## SPA fallback Since TMA.sh hosts Single Page Applications, it implements a fallback rule for client-side routing: **If the requested path does not match a known static file extension and the file does not exist in R2, serve `index.html` instead.** A hardcoded allowlist of static file extensions (e.g., `.js`, `.css`, `.png`, `.svg`, `.woff2`, `.ico`, etc.) is used to determine whether a request is for a static asset. Only requests matching these specific extensions bypass the SPA fallback and return a `404` if the file is not found. Requests for paths not matching the allowlist (including extensionless paths like `/settings`) are served `index.html`, allowing your client-side router (React Router, Vue Router, Svelte routing, etc.) to handle navigation. ``` myapp.tma.sh/settings → KV get "route:myapp" → { deploymentId: "", ... } → R2 get "deployments//settings" → not found → Path does not match static extension allowlist → SPA fallback → R2 get "deployments//index.html" → 200 OK ``` Files with known static extensions (`.js`, `.css`, `.png`, etc.) that don't exist in R2 return a `404` normally. ## Security headers Every response includes security headers configured for the Telegram Mini App environment: ### Content Security Policy The CSP allows loading the Telegram Web App SDK and communicating with Telegram's servers: ``` Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org https://*.telegram.org; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.tma.sh https://telegram.org https://*.telegram.org; img-src 'self' data: blob: https://telegram.org https://*.telegram.org; frame-ancestors https://web.telegram.org https://t.me; ``` ### Other headers | Header | Value | Purpose | |--------|-------|---------| | `X-Frame-Options` | `ALLOW-FROM https://web.telegram.org` | Permit embedding in Telegram's WebView | | `X-Content-Type-Options` | `nosniff` | Prevent MIME type sniffing | The CSP is intentionally permissive for `script-src` (allowing `unsafe-inline` and `unsafe-eval`) because many Telegram Mini App SDKs and frameworks require inline scripts. The `frame-ancestors` directive restricts embedding to Telegram's web client and `t.me`. ## Rollback Rollback is an instant operation. Since each deployment is an immutable set of files in R2 with its own deployment ID, rolling back means updating a single KV pointer: ``` Before rollback: KV "route:myapp" → { deploymentId: "", ... } After rollback: KV "route:myapp" → { deploymentId: "", ... } ``` This operation only updates routing metadata in KV. There is no rebuild and no asset re-upload. The route update then propagates globally through Cloudflare KV replication. Rollback is available through the project dashboard by selecting a previous deployment. ## Deployment cleanup To manage storage costs, TMA.sh runs a two-stage daily cleanup process: **Stage 1: Retention cleanup.** The daily cleanup job evaluates deployment history: - The **active production deployment** is never cleaned. - Among non-active terminal deployments (`ready`/`failed`), the most recent **10** are retained. - Older terminal deployments are cleaned and moved to `cleaned`. **Stage 2: Purge cleaned records.** On subsequent cleanup runs, deployments already in `cleaned` are finalized to `purged`. Preview deployments are also marked `cleaned` immediately when the staging PR is changed/closed. Cleaned/purged deployments keep metadata (commit, timestamps, logs) but are not rollback targets. --- # How It Works > Understanding the TMA.sh platform architecture TMA.sh turns a GitHub repository into a live Telegram Mini App. You connect a repo, link a Telegram bot, and push code. The platform handles the rest: building, hosting, CDN distribution, and bot configuration. ## Deployment flow Every deployment follows the same path from source code to a live Mini App: 1. **Connect** -- Link a GitHub repository and a Telegram bot to a TMA.sh project. 2. **Push** -- Push code to the repository. A GitHub webhook notifies TMA.sh. 3. **Build** -- A build container clones the repo, installs dependencies, and runs the build command. The output is a static directory (HTML, CSS, JS, assets). 4. **Upload** -- Built assets are uploaded to R2 (Cloudflare's object storage), and KV routing tables are updated to point the project's subdomain to the new deployment. 5. **Configure** -- The Telegram bot's menu button URL is updated automatically to point to the deployment. 6. **Live** -- The Mini App is accessible at `{project}.tma.sh`. ``` git push → GitHub webhook → Build container → R2 upload → KV routing → Live ``` Build duration depends on your repository size, dependency graph, and build commands. ## Deployment types TMA.sh supports two deployment types: **Production** -- Triggered by pushes to the default branch (usually `main`). The production deployment is what your users see. It is served at `{project}.tma.sh` and any custom domains you configure. **Preview** -- Triggered for the selected staging PR. You choose the target PR in project settings and click **Submit & Deploy**. The preview is served at `pr{number}--{project}.tma.sh`, and new commits to that selected PR trigger redeploys automatically. Preview deployments are useful for QA, stakeholder review, and testing changes before they reach production. See [Preview Environments](/concepts/preview-environments/) for details. ## Auto-deploy By default, every push to the default branch triggers a production deployment. This behavior is controlled by the **auto-deploy toggle** in project settings. When disabled, deployments must be triggered manually from the dashboard or CLI. Auto-deploy applies per-project. You can have some projects deploy on every push while others require manual intervention. ## Domain model The platform is organized around a few core entities: ``` Organization ├── Project │ ├── Deployment (production or preview) │ ├── Bot (production + optional preview/development bots) │ ├── Secret (environment variables, scoped by environment) │ └── Domain (custom domains) ``` ### Organizations Every user gets a **personal organization** on signup. All projects belong to an organization, not directly to a user. This means team collaboration is built in from day one -- invite members to your organization and they get access to all its projects. ### Projects A project represents a single Telegram Mini App. It is linked to one GitHub repository and one or more Telegram bots. Each project has its own subdomain (`{project}.tma.sh`), build configuration, secrets, and deployment history. ### Deployments A deployment is an immutable snapshot of your built application at a specific commit. Production deployments serve live traffic. Preview deployments are tied to the currently selected staging pull request. Recent terminal deployments are retained for rollback, while older ones move through cleanup. ### Bots Each project has at least one Telegram bot for production. You can also register a preview bot with `environment: 'preview'` so preview testing never affects your production bot's state or menu configuration. Bots can be scoped to one of three environments: `production`, `preview`, or `development`. ### Secrets Environment variables injected at build time. Secrets are scoped by environment (`production`, `preview`, or `development`), so you can use different API keys or configuration for testing versus production. ### Domains Projects are served at `{project}.tma.sh` by default. You can add custom domains (e.g., `app.example.com`) with automatic TLS provisioning. ## Infrastructure TMA.sh runs entirely on Cloudflare's infrastructure: | Component | Service | Purpose | |-----------|---------|---------| | API | Workers | Request handling, webhook processing, bot management | | Database | D1 | Organizations, projects, deployments, bots, secrets | | Assets | R2 | Built application files (HTML, JS, CSS, images) | | Routing | KV | Subdomain-to-deployment mapping, fast lookups | | Build queue | Queues | Ordered build job processing | | Build execution | Containers | Isolated build environments | | User API routes | Workers for Platforms | Per-project server-side API endpoints | This architecture means deployments are globally distributed with no single point of failure, and scaling is handled automatically. --- # Preview Environments > Controlled staging deployments for a selected pull request Preview environments provide a controlled staging lane for pull requests. Each project can have **one selected PR** for staging at a time. In the dashboard, choose a PR and click **Submit & Deploy**: - TMA.sh validates the PR on GitHub. - The PR is saved as the project's staging target. - A preview deployment is triggered immediately from that PR's current head commit. After that, new commits to the selected PR trigger automatic preview redeploys. ## URL pattern Preview deployments are served at: ``` pr{number}--{project}.tma.sh ``` For example, pull request #42 on a project called `myapp` is accessible at: ``` pr42--myapp.tma.sh ``` If your project includes API routes, preview API traffic is available at: ``` pr{number}--{project}--api.tma.sh/* ``` ## Lifecycle A preview environment follows the lifecycle of the selected staging PR: ### 1. Select PR and submit From **Project Settings → Deployment**, choose an open PR (or enter a PR number), then click **Submit & Deploy**. ``` Select PR → Submit & Deploy → Deployment created (type: preview) ``` ### 2. Build The preview deployment goes through the same [build pipeline](/concepts/build-pipeline/) as production: dependency installation, build execution, output validation, asset upload, and route activation. ### 3. Bot assignment Preview deployments can use a Telegram bot registered with `environment: 'preview'`. This bot is separate from the production bot, so testing interactions in preview does not affect live users. There is one preview bot per project/environment. The same preview bot is reused across preview deployments, and its menu button URL is updated to the current preview URL (`pr{number}--{project}.tma.sh`). ### 4. Selected PR updated When new commits are pushed to the selected PR branch, the preview deployment is rebuilt automatically. The URL stays the same (`pr{number}--{project}.tma.sh`) and serves the latest build. ### 5. Switch staging PR (optional) If you switch staging from PR A to PR B and submit, PR A's preview routing is removed immediately. Staging now points to PR B once its deployment is ready. ### 6. PR closed or merged When the pull request is closed or merged: - The KV routing entries for the preview are removed. - The preview deployments are marked as `cleaned`. There is no Telegram API call to deactivate the preview bot -- it simply becomes unreachable because the routing entries no longer exist. If the PR is merged, the production deployment is triggered separately by the push to the default branch. ## Preview-scoped secrets Secrets in TMA.sh are scoped by environment (`production`, `preview`, `development`). In practice, you define separate entries per environment (often reusing the same key name with different values). This is useful for: - **API keys** -- Use test/sandbox API keys in preview, production keys in production. - **Database URLs** -- Point previews at a staging database. - **Feature flags** -- Enable experimental features only in preview. ``` Project secrets: DATABASE_URL (production) → postgres://prod-db/myapp DATABASE_URL (preview) → postgres://staging-db/myapp STRIPE_KEY (production) → sk_live_... STRIPE_KEY (preview) → sk_test_... ``` Preview deployments only receive secrets scoped to `preview`. They never have access to production secrets. ## Use cases ### QA testing Share the preview URL and bot with your QA team. They can test the Mini App in a real Telegram environment without affecting production. ### Stakeholder review Non-technical stakeholders can open the preview bot in Telegram and interact with the app directly. No setup, no local environment, no CLI tools required. ### Branch experimentation Run focused staging reviews on one PR at a time. Switch the staging target when you want to review a different branch. ### Bot interaction testing Using a preview bot, you can test bot commands, inline queries, and webhook handlers without affecting the production bot. ## Comparison with production | Aspect | Production | Preview | |--------|-----------|---------| | URL | `{project}.tma.sh` | `pr{number}--{project}.tma.sh` | | Trigger | Push to default branch | Submit selected PR, then PR updates | | Bot | Production bot | Optional preview bot (`environment: 'preview'`) | | Secrets | Production-scoped | Preview-scoped | | Custom domains | Supported | Not supported | | Active target | One production deployment | One selected staging PR per project | | Cleanup | Active production + latest retained window | Marked `cleaned` when PR is closed/switched; also subject to retention cleanup | | Rollback | Supported | Not applicable | ## Disabling previews Set staging PR selection to **None** in project settings. Pull requests will no longer trigger preview deployments until a PR is selected again. --- # CLI Overview > The TMA command-line interface The `tma` CLI is the primary tool for developing and deploying Telegram Mini Apps on TMA.sh. It handles project scaffolding, local development, deployment, environment variable management, and bot configuration. ## Installation ```bash bun add -g @tma.sh/cli ``` Verify the installation: ```bash tma --version ``` ## Commands | Command | Description | |---------|-------------| | `tma login` | Authenticate with TMA.sh | | `tma logout` | Clear stored credentials | | `tma init [name]` | Create a new project | | `tma link` | Link current directory to existing project | | `tma dev` | Start local development server | | `tma deploy` | Deploy to production | | `tma env` | Manage environment variables | | `tma logs` | View deployment build logs | | `tma bot` | Configure Telegram bot settings | Run `tma --help` for the command list, then use this docs section for per-command reference. ## Project configuration Both `tma init` and `tma link` create `.tma/project.json`, but with different shapes: `tma init` writes a local scaffold config: ```json { "projectName": "my-app" } ``` `tma link` writes the full linked-project config used by authenticated commands: ```json { "projectId": "11111111-2222-4333-8444-555555555555", "orgId": "66666666-7777-4888-8999-000000000000", "projectName": "my-app" } ``` Run `tma link` after `tma init` before using commands that require a linked project (`deploy`, `env`, `logs`, `bot`). Commit `.tma/project.json`, but ignore local runtime artifacts under `.tma/`: ```gitignore .tma/* !.tma/project.json ``` See [Commands](/cli/commands/) for the full reference on each command. --- # CLI Commands > Complete reference for all TMA CLI commands Detailed reference for every command available in the `tma` CLI. ## `tma login` Authenticate with TMA.sh using the device code flow. ```bash tma login ``` The CLI opens your browser and displays a one-time code. Enter the code in the browser to authorize the session. Once confirmed, credentials are stored locally and used for all subsequent commands. Credentials are saved to `~/.tma/credentials.json`. They persist across sessions until you run `tma logout` or they expire. ## `tma logout` Clear stored authentication credentials. ```bash tma logout ``` Removes the local credentials file. You will need to run `tma login` again before using any authenticated commands. ## `tma init [project-name]` Create a new TMA.sh project with interactive scaffolding. ```bash tma init my-app ``` If `project-name` is omitted, the CLI prompts you for one. The scaffolding wizard walks through the following: 1. **Template selection** -- choose from Vite (React, Vue, or Svelte) or Plain HTML. 2. **API routes** -- optionally include a `server/api/index.ts` file for edge API routes (Hono scaffolded by default, but any fetch-compatible framework works). 3. **Bot handlers** -- optionally include a `bot/index.ts` file for Telegram bot command handlers. 4. **Dependency installation** -- runs `bun install` when Bun is available, otherwise falls back to `npm install`. The command creates the project directory, writes all template files, and creates a local `.tma/project.json` containing only `projectName`. ``` my-app/ .tma/ project.json src/ ... server/ # only if API routes selected api/ index.ts bot/ # only if bot handlers selected index.ts index.html package.json ``` After scaffolding, run `tma link` from inside the project directory to write the full linked-project config (`projectId`, `orgId`, `projectName`) required by authenticated commands. ## `tma link` Link the current directory to an existing TMA.sh project. ```bash tma link ``` Use this when you have an existing codebase that you want to deploy to TMA.sh, or when cloning a repo that does not yet have a `.tma/project.json` file. The CLI fetches your projects and prompts you to select one. You can also choose **+ Create new project** in the prompt. It then writes `.tma/project.json` with `projectId`, `orgId`, and `projectName`. ## `tma dev` Start the local development environment. ```bash tma dev ``` This command starts everything you need for local development: - **Vite dev server** on port `5173` with hot module replacement. - **API routes** (if `server/api/index.ts` exists): starts an esbuild watcher and Miniflare on port `8787`. - **Bot handlers** (if `bot/index.ts` exists): starts Miniflare on port `8788` for bot webhook processing. Vite proxies `/api/*` and `/bot/*` requests to the respective Miniflare instances so your frontend, API, and bot handlers share the same origin during development. ## `tma deploy` Trigger a manual deployment to production. ```bash tma deploy ``` Triggers a server-side deployment by calling the TMA.sh API, then polls for completion. The CLI does not build locally or upload assets -- all building happens on the server. `tma deploy` requires: - A linked project config from `tma link` (`.tma/project.json` with `projectId` and `orgId`) - A repository connected to that project in the dashboard (the build runs from GitHub) This command is for manual deployments. If you have auto-deploy enabled (the default), pushing to your `main` branch on GitHub triggers a deployment automatically without needing to run this command. The CLI streams build status to your terminal and prints the live URL when the deployment is complete: ``` Deployment triggered... Deployment status: building Deployment live at https://my-app.tma.sh ``` ### Options | Flag | Description | |------|-------------| | `--preview` | Trigger a preview deployment for the currently selected staging PR (set in dashboard) | ## `tma env` Manage environment variables and secrets for your project. ```bash # List all environment variables tma env list # Set a variable (use = between key and value) tma env set API_KEY=sk-abc123 # Remove a variable tma env remove API_KEY # Pull env vars to a local .env.local file (values are redacted) tma env pull ``` All environment variables are set for the `production` environment. Environment scoping for preview and development is managed through the dashboard. ### `tma env pull` Writes a `.env.local` file to the current directory containing the secret key names from your project. Values are redacted by the API -- this is useful for seeing which variables are configured without exposing actual secrets. ### Security All environment variables are encrypted at rest. During deployments they are provided to build commands and worker bindings. In API routes, access them from worker bindings (`env` / `c.env`), not `process.env`. Avoid logging secret values from your own code. ## `tma logs` View build logs for recent deployments. ```bash # View logs for the latest deployment tma logs # View logs for a specific deployment tma logs --deployment 11111111-2222-3333-4444-555555555555 # Stream logs in real time tma logs --follow ``` ### Options | Flag | Description | |------|-------------| | `--deployment ` | View logs for a specific deployment | | `--follow` | Stream logs in real time | Displays build output, status transitions, and errors. Useful for debugging failed deployments. ## `tma bot` Manage Telegram bots registered with your TMA.sh project. ```bash # List all registered bots (default when no subcommand is given) tma bot tma bot list # Register a new bot tma bot register # Remove a registered bot tma bot remove # Check bot status tma bot status ``` ### Subcommands | Subcommand | Description | |------------|-------------| | `list` | List all registered bots for the project (default) | | `register` | Register a Telegram bot with TMA.sh. Prompts for the bot token and environment selection (`production`, `preview`, or `development`). | | `remove` | Remove a registered bot from the project | | `status` | Check the status of registered bots | Running `tma bot` with no subcommand is equivalent to `tma bot list`. --- # SDK Overview > The TMA.sh SDK for Telegram Mini Apps `@tma.sh/sdk` provides everything you need to build authenticated, payment-ready Telegram Mini Apps. Install one package and get auth validation, signed JWTs, TON and Stars payments, and key-value storage -- all wired to your TMA.sh project automatically. ## Installation ```bash bun add @tma.sh/sdk ``` ## Quick start ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'your-project-id' }); // Authenticate the user const { user, jwt } = await tma.auth.validate( window.Telegram.WebApp.initData, 'your-project-id' ); // Use KV storage await tma.kv.set('key', { value: 'data' }); const data = await tma.kv.get('key'); // Auto-authenticated fetch (injects JWT Authorization header) const response = await tma.fetch('/api/profile'); ``` `createTMA()` accepts optional config (`projectId`, `apiUrl`). The SDK client talks to `https://api.tma.sh` by default, and auth/KV/payment calls are scoped by the JWT and provided `projectId`. ## Package exports The SDK ships multiple entry points so you only import what you need: | Import | Purpose | |--------|---------| | `@tma.sh/sdk` | Core client -- auth, payments, KV storage | | `@tma.sh/sdk/react` | React hooks and providers | | `@tma.sh/sdk/svelte` | Svelte reactive stores | | `@tma.sh/sdk/bot` | Bot handler utilities (defineBot, session middleware) | | `@tma.sh/sdk/server` | Server-side helpers for API routes | The core package is framework-agnostic. Use it directly in any JavaScript project. The framework-specific entry points (`/react`, `/svelte`) provide idiomatic bindings that handle loading states, reactivity, and error handling for you. The `/bot` entry point provides utilities for building Telegram bot handlers with session middleware. The `/server` entry point is designed for [API routes](/guides/api-routes/) that run on the edge. It includes Hono auth middleware, a typed KV wrapper, and `initData` validation utilities. The KV wrapper, `validateInitData`, and Telegram API client are framework-agnostic; `requireUser()` is a Hono middleware. ## What's inside ### Authentication Validate Telegram users with a single call. The SDK verifies `initData` via HMAC-SHA256 and returns a signed JWT that works with any backend -- Supabase, Firebase, Turso, or your own. See [Authentication](/sdk/auth/) for details. ### Payments Accept Telegram Stars payments with a single call via `tma.payments.stars.pay()`. The SDK handles invoice creation through the platform (no bot token needed client-side) and opens the native Telegram payment sheet. TON Connect cryptocurrency payments are also supported via `@tonconnect/ui`. See [Payments](/sdk/payments/) for details. ### KV Storage Simple key-value storage scoped to your project. Store JSON-serializable values up to 128 KB per key. No database setup, no connection strings. See [KV Storage](/sdk/kv/) for details. ### Server Helpers Auth middleware (Hono), typed KV bindings, and `initData` validation for your API routes. The KV wrapper and validation helpers work with any framework. See [Server Helpers](/sdk/server/) for details. --- # Authentication > Validate Telegram users and get signed JWTs TMA.sh authentication turns Telegram's `initData` string into a verified user identity and a signed JWT. No passwords, no OAuth flows -- the user is already authenticated by Telegram. ## How it works 1. Telegram injects `initData` into the WebApp context when a user opens your Mini App. 2. Your app sends `initData` to TMA.sh via the SDK. 3. TMA.sh validates the HMAC-SHA256 signature against your bot token, confirming the data came from Telegram and has not been tampered with. 4. TMA.sh returns a signed JWT containing the user's Telegram identity. 5. Your app uses that JWT to authenticate API requests to your own backend or third-party services. The entire flow is a single function call on the client. ## Client-side usage ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'your-project-id' }); const { user, jwt } = await tma.auth.validate( window.Telegram.WebApp.initData, 'your-project-id' ); // user: { telegramId, firstName, lastName, username } // jwt: signed token (24h expiry) ``` The returned `jwt` is a compact JWS signed by the TMA platform key. Include it as a Bearer token in requests to your API routes or external backends. ## JWT claims Every JWT issued by TMA.sh includes the following claims: | Claim | Description | Example | |-------|-------------|---------| | `sub` | Subject identifier | `tg_123456789` | | `telegramId` | Telegram user ID | `123456789` | | `firstName` | User's first name | `Alice` | | `lastName` | User's last name (may be empty) | `Smith` | | `username` | Telegram username (may be empty) | `alicesmith` | | `projectId` | TMA.sh project ID | `11111111-2222-4333-8444-555555555555` | | `iat` | Issued at (Unix timestamp) | `1700000000` | | `exp` | Expiration (Unix timestamp, 24h) | `1700086400` | The `sub` claim is prefixed with `tg_` to avoid collisions when integrating with external auth systems. ## React hook The `useTelegramAuth` hook handles the full auth lifecycle -- validation, loading state, and error handling: ```tsx import { TMAProvider, useTelegramAuth } from '@tma.sh/sdk/react'; function App() { return ( ); } function Profile() { const { user, jwt, isLoading, error } = useTelegramAuth(); if (isLoading) return
Loading...
; if (error) return
Auth failed: {error.message}
; return
Welcome, {user.firstName}!
; } ``` Wrap your app in `` once at the root, passing your `projectId` via the `config` prop. The provider initializes the SDK and makes auth state available to all child components. ## Svelte store Initialize the TMA provider once in your root layout, then use the `getTMAAuth` function to access the reactive auth store in any component: ```svelte {#if $auth.isLoading}

Loading...

{:else if $auth.user}

Welcome, {$auth.user.firstName}!

{/if} ``` The store subscribes to auth state changes and updates the UI automatically. Check `$auth.user` to determine if the user is authenticated -- there is no `isAuthenticated` property. ## Supabase integration You can forward the TMA JWT to Supabase as a Bearer token: ```typescript import { createClient } from '@supabase/supabase-js'; const supabase = createClient( 'https://your-project.supabase.co', 'your-anon-key', { global: { headers: { Authorization: `Bearer ${jwt}` }, }, } ); // Queries run with the forwarded JWT const { data } = await supabase .from('profiles') .select('*') .eq('user_id', user.telegramId); ``` If you want Supabase RLS to validate TMA tokens directly, configure Supabase to trust TMA-issued JWTs via the public JWKS endpoint (`https://api.tma.sh/.well-known/jwks.json`). This pattern also works with any backend that supports standard JWT verification. ## JWKS endpoint TMA.sh exposes a single JWKS endpoint for custom backend verification: ``` https://api.tma.sh/.well-known/jwks.json ``` Use this to verify JWTs from your own server without sharing secrets. Most JWT libraries support JWKS out of the box: ```typescript import { createRemoteJWKSet, jwtVerify } from 'jose'; const JWKS = createRemoteJWKSet( new URL('https://api.tma.sh/.well-known/jwks.json') ); const { payload } = await jwtVerify(token, JWKS); // payload.sub === 'tg_123456789' ``` ## Server-side middleware For Hono-based API routes deployed on TMA.sh, use the `requireUser()` Hono middleware instead of manual JWT verification. It extracts the Bearer token, verifies it via JWKS, and injects the user into the Hono context. If you use a different framework, you can perform the same JWT verification manually using `jose` as shown above. See [Server Helpers](/sdk/server/) for usage. --- # Database > D1 database binding API reference Projects on a Pro or Team plan get a managed D1 (SQLite) database. The database is available in API routes as the `DB` binding -- a standard Cloudflare `D1Database` instance. See the [Managed Database guide](/guides/database/) for setup instructions. ## Binding type The `DB` binding is a `D1Database` object from the Cloudflare Workers runtime. Declare it in your Hono env type: ```typescript type Env = { Bindings: { DB: D1Database; KV: KVNamespace; }; }; const app = new Hono(); ``` With a plain fetch handler, access it from the `env` parameter: ```typescript export default { async fetch(request: Request, env: { DB: D1Database }) { const result = await env.DB.prepare('SELECT 1').first(); return Response.json(result); }, }; ``` The `DB` binding is only present when the project has a provisioned database (i.e., you have deployed with `db/migrations/` files at least once). If no database exists, `env.DB` is `undefined`. ## Raw D1 API The D1 API provides three execution methods on prepared statements: ### `prepare(sql).bind(...params)` Create a prepared statement with parameter binding. Always use `.bind()` for user-provided values to prevent SQL injection: ```typescript app.get('/api/users/:id', async (c) => { const id = c.req.param('id'); const user = await c.env.DB.prepare( 'SELECT * FROM users WHERE telegram_id = ?' ) .bind(id) .first(); if (!user) { return c.json({ error: 'Not found' }, 404); } return c.json(user); }); ``` ### `.first(column?)` Returns the first row, or a single column value if `column` is specified: ```typescript // First row as an object const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?') .bind(1) .first(); // { id: 1, telegram_id: 12345, username: 'alice', score: 42 } // Single column value const count = await c.env.DB.prepare('SELECT COUNT(*) as total FROM users') .first('total'); // 150 ``` ### `.all()` Returns all matching rows: ```typescript const result = await c.env.DB.prepare( 'SELECT * FROM users ORDER BY score DESC LIMIT 50' ).all(); // result.results = [{ id: 1, ... }, { id: 2, ... }, ...] // result.success = true // result.meta = { duration: 0.5, ... } ``` ### `.run()` Execute a statement that does not return rows (INSERT, UPDATE, DELETE): ```typescript await c.env.DB.prepare('INSERT INTO users (telegram_id, username) VALUES (?, ?)') .bind(12345, 'alice') .run(); ``` ### `.batch(statements)` Execute multiple statements in a single round-trip. All statements in a batch run inside an implicit transaction: ```typescript const results = await c.env.DB.batch([ c.env.DB.prepare('INSERT INTO users (telegram_id, username) VALUES (?, ?)') .bind(12345, 'alice'), c.env.DB.prepare('INSERT INTO items (user_id, name, rarity) VALUES (?, ?, ?)') .bind(1, 'Sword', 'rare'), ]); ``` ## Using with Drizzle ORM For type-safe queries, use `drizzle-orm` with the D1 driver: ```typescript import { drizzle } from 'drizzle-orm/d1'; import { eq } from 'drizzle-orm'; import * as schema from '../../db/schema'; app.get('/api/users/:telegramId', async (c) => { const db = drizzle(c.env.DB, { schema }); const telegramId = Number(c.req.param('telegramId')); const user = await db .select() .from(schema.users) .where(eq(schema.users.telegramId, telegramId)) .get(); return c.json(user ?? { error: 'Not found' }); }); ``` ### Insert and return ```typescript app.post('/api/users', async (c) => { const db = drizzle(c.env.DB, { schema }); const body = await c.req.json(); const user = await db .insert(schema.users) .values({ telegramId: body.telegramId, username: body.username, }) .returning() .get(); return c.json(user, 201); }); ``` ### Update ```typescript app.put('/api/users/:telegramId/score', async (c) => { const db = drizzle(c.env.DB, { schema }); const telegramId = Number(c.req.param('telegramId')); const { score } = await c.req.json(); const updated = await db .update(schema.users) .set({ score }) .where(eq(schema.users.telegramId, telegramId)) .returning() .get(); return c.json(updated); }); ``` ### Delete ```typescript app.delete('/api/items/:id', async (c) => { const db = drizzle(c.env.DB, { schema }); const id = Number(c.req.param('id')); await db.delete(schema.items).where(eq(schema.items.id, id)).run(); return c.json({ ok: true }); }); ``` ### Relational queries ```typescript app.get('/api/users/:telegramId/items', async (c) => { const db = drizzle(c.env.DB, { schema }); const telegramId = Number(c.req.param('telegramId')); const user = await db.query.users.findFirst({ where: eq(schema.users.telegramId, telegramId), with: { items: true, }, }); return c.json(user); }); ``` Relational queries require that you pass `{ schema }` when creating the Drizzle instance and that your schema defines `relations()`. See the [Drizzle ORM docs](https://orm.drizzle.team/docs/rqb) for details. ## Type safety Define your binding types once and reuse them across your API routes: ```typescript import { Hono } from 'hono'; type Env = { Bindings: { DB: D1Database; KV: KVNamespace; BOT_TOKEN: string; }; }; const app = new Hono(); ``` When using Drizzle ORM, your schema definitions serve as the source of truth for table types. Use `typeof schema.users.$inferSelect` and `typeof schema.users.$inferInsert` for inferred row types: ```typescript import * as schema from '../../db/schema'; type User = typeof schema.users.$inferSelect; type NewUser = typeof schema.users.$inferInsert; ``` ## Specifications | Property | Limit | |----------|-------| | Max query execution time | 30 seconds | | Max database size | 10 GB | | Max bound parameters per query | 100 | | Consistency | Strong (single-region SQLite) | D1 provides strong read-after-write consistency -- unlike KV, a read immediately after a write always returns the latest value. --- # KV Storage > Key-value storage API reference Every TMA.sh project includes a key-value store. No database setup, no connection strings -- import the SDK and start reading and writing data. ## Client-side API ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'your-project-id' }); ``` ### Set a value Store any JSON-serializable value: ```typescript await tma.kv.set('user:123:preferences', { theme: 'dark', language: 'en', notifications: true, }); ``` Setting a key that already exists overwrites the previous value. ### Get a value Retrieve a value by key. Returns `null` if the key does not exist: ```typescript const prefs = await tma.kv.get('user:123:preferences'); // { theme: 'dark', language: 'en', notifications: true } ``` ### Remove a value Remove a key and its value: ```typescript await tma.kv.remove('user:123:preferences'); ``` Removing a key that does not exist is a no-op. ### List keys by prefix Retrieve all keys matching a prefix: ```typescript const result = await tma.kv.list('user:123:'); const keyNames = result.keys.map(k => k.name); // ['user:123:preferences', 'user:123:scores', 'user:123:inventory'] ``` The `list()` method returns an object with `keys` (an array of `{ name: string }` objects) and `list_complete` (a boolean indicating whether all matching keys have been returned). Use colons as separators to create a natural key hierarchy. Fetch individual values with `kv.get()`. ## Specifications | Property | Limit | |----------|-------| | Max value size | 128 KB | | Max key size | 512 bytes | | Consistency | Eventually consistent (~60s) | Values are stored as JSON. The 128 KB limit applies to the serialized JSON string. Keys must be valid UTF-8 strings. Eventually consistent means that after a write, subsequent reads from different edge locations may return the previous value for up to 60 seconds. Reads from the same location that performed the write are immediately consistent. ## Key naming conventions Use colon-separated segments for structured keys: ``` {entity}:{id}:{field} ``` Examples: ``` user:123:preferences user:123:scores leaderboard:global session:abc123 feature:dark-mode cache:api:weather:london ``` This convention makes prefix-based listing predictable and keeps your keyspace organized. ## Use cases KV storage works well for: - **User preferences** -- theme, language, notification settings - **Feature flags** -- toggle features per user or globally - **Leaderboards** -- store scores keyed by user ID, list by prefix - **Session data** -- temporary state between page loads - **Cached API responses** -- store third-party API results with a TTL key pattern ## When to use something else KV storage is not a database. For the following use cases, use the [managed D1 database](/guides/database/) (Pro and Team plans) or connect an external database like Supabase or Turso: - **Relational data** -- queries that join tables or filter on multiple columns - **Transactions** -- operations that must succeed or fail atomically - **Complex queries** -- full-text search, aggregations, sorting by multiple fields - **Strong consistency** -- reads that must always return the latest write See [Database](/sdk/database/) for the D1 binding API reference, or the [Authentication](/sdk/auth/) page for how to connect TMA.sh JWTs to Supabase. ## Server-side usage In API routes, use the `createKV()` helper from `@tma.sh/sdk/server` for a typed wrapper around the KV binding. See [Server Helpers](/sdk/server/) for details. --- # Payments > Accept TON and Telegram Stars payments TMA.sh supports two payment methods: **Telegram Stars** for Telegram's in-app currency and **TON Connect** for cryptocurrency payments. ## Telegram Stars Telegram Stars is Telegram's built-in digital currency. The SDK provides a first-class `payments` namespace that handles invoice creation, opening the payment sheet, and tracking the result -- all without exposing your bot token to the client. ### One-liner: `tma.payments.stars.pay()` The simplest way to accept Stars payments. Creates an invoice via the platform endpoint, opens the native Telegram payment sheet, and returns the result: ```typescript import { createTMA } from '@tma.sh/sdk'; const tma = createTMA({ projectId: 'my-project' }); const result = await tma.payments.stars.pay({ title: 'Premium Subscription', description: 'Access premium features for 30 days', payload: JSON.stringify({ userId: user.telegramId, plan: 'premium' }), prices: [{ label: 'Premium (30 days)', amount: 100 }], }); if (result.status === 'paid') { // Payment successful } ``` The `pay()` method: 1. Sends the invoice params to `POST /sdk/v1/payments/stars/invoice` (the platform uses your project's stored bot token) 2. Opens the native Telegram payment sheet via `WebApp.openInvoice()` 3. Returns a `StarsPaymentResult` with `status: 'paid' | 'cancelled' | 'failed' | 'pending'` ### Create invoice separately If you need the invoice URL without immediately opening it (for example, to share or store it): ```typescript const invoiceUrl = await tma.payments.stars.createInvoice({ title: 'Premium Subscription', description: 'Access premium features for 30 days', payload: JSON.stringify({ plan: 'premium' }), prices: [{ label: 'Premium (30 days)', amount: 100 }], }); // Open it later window.Telegram.WebApp.openInvoice(invoiceUrl, (status) => { if (status === 'paid') { /* ... */ } }); ``` ### Invoice parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `title` | `string` | Yes | Product name (max 32 chars) | | `description` | `string` | Yes | Product description (max 255 chars) | | `payload` | `string` | Yes | Opaque data returned in webhooks (max 128 chars) | | `prices` | `LabeledPrice[]` | Yes | Array of `{ label, amount }` where amount is in Stars | | `photoUrl` | `string` | No | URL of product photo | | `photoWidth` | `number` | No | Photo width in pixels | | `photoHeight` | `number` | No | Photo height in pixels | ### React hook ```tsx import { useStarsPayment, createTMA } from '@tma.sh/sdk/react'; const tma = createTMA({ projectId: 'my-project' }); function PurchaseButton() { const { pay, status, isLoading, error } = useStarsPayment(); const handlePurchase = async () => { await pay(tma, { title: 'Premium', description: '30-day access', payload: JSON.stringify({ plan: 'premium' }), prices: [{ label: 'Premium', amount: 100 }], }); }; return ( ); } ``` ### Svelte ```svelte ``` ### Server-side: typed methods If you need to create invoices or handle pre-checkout queries in your own API routes (rather than via the platform endpoint), use the typed methods on `TelegramApiClient`: ```typescript import { createTelegramApiClient } from '@tma.sh/sdk/server'; const telegram = createTelegramApiClient(c.env.BOT_TOKEN); // Typed method (preferred over generic call()) const invoiceUrl = await telegram.createInvoiceLink({ title: 'Premium', description: '30 days of premium', payload: JSON.stringify({ plan: 'premium' }), currency: 'XTR', prices: [{ label: 'Premium', amount: 100 }], }); // Answer pre-checkout query (must respond within 10 seconds) await telegram.answerPreCheckoutQuery(query.id, true); ``` ### Bot context: `sendInvoice` In your bot handler, send an invoice directly to a chat: ```typescript import { defineBot } from '@tma.sh/sdk/bot'; export default defineBot({ commands: [ { command: 'buy', description: 'Purchase premium', handler: async (ctx) => { await ctx.sendInvoice({ title: 'Premium', description: '30-day access', payload: JSON.stringify({ userId: ctx.from?.id }), currency: 'XTR', prices: [{ label: 'Premium', amount: 100 }], }); }, }, ], onPreCheckoutQuery: async (ctx) => { await ctx.answerPreCheckoutQuery(true); }, }); ``` ### Handle payment webhooks After a successful Stars payment, Telegram sends a `pre_checkout_query` and then a `successful_payment` update to your bot. TMA.sh automatically tracks `stars_payment` analytics events with campaign attribution when payments complete through the bot webhook. ## TON Connect TON Connect lets users pay with TON cryptocurrency from their wallet. Use the `@tonconnect/ui` library directly -- TON Connect is independent of the TMA SDK. ### Installation ```bash bun add @tonconnect/ui ``` ### Connect a wallet Before sending a transaction, the user must connect their TON wallet: ```typescript import { TonConnectUI } from '@tonconnect/ui'; const tonConnect = new TonConnectUI({ manifestUrl: 'https://your-app.tma.sh/tonconnect-manifest.json', }); // Opens the TON Connect wallet selector await tonConnect.connectWallet(); ``` This opens the standard TON Connect modal. The user selects their wallet app (Tonkeeper, MyTonWallet, etc.) and approves the connection. ### Send a transaction Once connected, send a transaction with the destination address and amount in nanotons: ```typescript const result = await tonConnect.sendTransaction({ validUntil: Math.floor(Date.now() / 1000) + 300, // 5 minutes messages: [ { address: 'UQ...', // recipient address amount: '1500000000', // nanotons (1.5 TON) }, ], }); ``` The user sees a confirmation screen in their wallet app before the transaction is submitted. ### Disconnect To disconnect the wallet (for example, when the user logs out): ```typescript await tonConnect.disconnect(); ``` ### Amount conversion TON amounts are specified in **nanotons** (1 TON = 1,000,000,000 nanotons). Some common values: | TON | Nanotons | |-----|----------| | 0.1 | `100000000` | | 1.0 | `1000000000` | | 5.0 | `5000000000` | | 10.0 | `10000000000` | ## Choosing a payment method | | TON Connect | Telegram Stars | |---|---|---| | Currency | TON cryptocurrency | Telegram Stars | | User experience | Wallet confirmation | Native Telegram payment sheet | | Server required | No (client-side) | Yes (invoice creation via API route) | | Settlement | On-chain (instant) | Telegram balance | | SDK dependency | `@tonconnect/ui` (independent) | `@tma.sh/sdk/server` | | Best for | Crypto-native users, NFTs | Subscriptions, in-app items | --- # Server Helpers > SDK utilities for API routes The `@tma.sh/sdk/server` entry point provides utilities for [API routes](/guides/api-routes/) that run on the edge alongside your static app. These helpers handle auth middleware, typed KV access, `initData` validation, and Telegram Bot API calls. :::note `requireUser()` is a Hono middleware. The other helpers -- `createKV`, `validateInitData`, and `createTelegramApiClient` -- are framework-agnostic and work with any fetch-compatible setup. ::: ```typescript import { requireUser, createKV, validateInitData, createTelegramApiClient, } from '@tma.sh/sdk/server'; ``` ## `requireUser()` Hono middleware that authenticates requests using the TMA.sh JWT from the `Authorization` header. Apply it to any route that requires an authenticated user. ```typescript import { Hono } from 'hono'; import { requireUser } from '@tma.sh/sdk/server'; const app = new Hono(); // Protect all /api/* routes app.use('/api/*', requireUser()); app.get('/api/me', (c) => { const user = c.get('user'); return c.json({ telegramId: user.telegramId, firstName: user.firstName, lastName: user.lastName, username: user.username, }); }); ``` ### How it works 1. Extracts the `Bearer` token from the `Authorization` header. 2. Verifies the JWT signature using locally injected `TMA_JWKS` (when available) or the default public JWKS endpoint. 3. Checks that the token has not expired. 4. Injects the decoded user object into the Hono context via `c.get('user')`. If the token is missing, invalid, or expired, the middleware returns a `401 Unauthorized` response automatically. ### User object The `user` object injected by `requireUser()` contains: | Property | Type | Description | |----------|------|-------------| | `telegramId` | `number` | Telegram user ID | | `firstName` | `string` | User's first name | | `lastName` | `string` | User's last name (may be empty) | | `username` | `string` | Telegram username (may be empty) | ## `createKV(binding)` Typed wrapper around the Cloudflare KV binding. Provides the same `get`, `set`, `delete`, and `list` API as the client-side SDK, with TypeScript generics for value types. ```typescript import { createKV } from '@tma.sh/sdk/server'; app.get('/api/score', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user'); const score = await kv.get(`score:${user.telegramId}`); return c.json({ score }); }); app.post('/api/score', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user'); const { score } = await c.req.json(); await kv.set(`score:${user.telegramId}`, score); return c.json({ ok: true }); }); ``` The generic parameter on `kv.get()` narrows the return type. If the key does not exist, it returns `null`. ### Methods | Method | Signature | Description | |--------|-----------|-------------| | `get` | `get(key: string): Promise` | Retrieve a value | | `set` | `set(key: string, value: unknown, ttl?: number): Promise` | Store a value (optional TTL in seconds) | | `delete` | `delete(key: string): Promise` | Remove a key | | `list` | `list(prefix?: string): Promise` | List key names by optional prefix | The server-side `set()` method accepts an optional third argument for TTL (time-to-live) in seconds. The client-side SDK does not support TTL. Note that the server-side method is `delete()`, while the client-side SDK uses `remove()`. Also, server-side `list()` returns `string[]`, while client-side `tma.kv.list()` returns `{ keys, list_complete }`. ## `validateInitData(initData, botToken)` Performs local HMAC-SHA256 verification of Telegram's `initData` string. No external API call -- the validation runs entirely on the edge. ```typescript import { validateInitData } from '@tma.sh/sdk/server'; app.post('/api/verify', async (c) => { const { initData } = await c.req.json(); const verified = await validateInitData(initData, c.env.BOT_TOKEN); if (!verified) { return c.json({ error: 'Invalid initData' }, 401); } // verified is a flat object with camelCase fields: // verified.telegramId (number) // verified.firstName (string) // verified.lastName (string | undefined) // verified.username (string | undefined) // verified.authDate (number) return c.json({ telegramId: verified.telegramId, firstName: verified.firstName, authDate: verified.authDate, }); }); ``` ### Validation steps 1. Parses the `initData` query string. 2. Computes the HMAC-SHA256 hash using the bot token as the secret key. 3. Compares the computed hash against the `hash` field in `initData`. 4. Checks that `auth_date` is within a 5-minute window to prevent replay attacks. Returns a `ValidatedInitData` object on success, or `null` if validation fails. ### When to use this Use `validateInitData()` when you need to verify `initData` directly in your API route without going through the full JWT flow. This is useful for: - Initial authentication before issuing your own tokens - Webhook handlers that receive `initData` from the client - Custom auth flows that do not use the SDK's built-in JWT system For most use cases, prefer `requireUser()` -- it handles token verification automatically and gives you a clean user object. ## `createTelegramApiClient(botToken)` A client for the Telegram Bot API. Used primarily for [Stars payments](/sdk/payments/) but supports any Bot API method via the generic `call()` method. ```typescript import { createTelegramApiClient } from '@tma.sh/sdk/server'; app.post('/api/notify', async (c) => { const telegram = createTelegramApiClient(c.env.BOT_TOKEN); const user = c.get('user'); await telegram.sendMessage(user.telegramId, 'Your order is ready!'); return c.json({ ok: true }); }); ``` ### Methods | Method | Signature | Description | |--------|-----------|-------------| | `call` | `call(method: string, params: Record): Promise` | Call any Telegram Bot API method by name | | `sendMessage` | `sendMessage(chatId: number, text: string, options?): Promise` | Send a text message to a chat | | `sendPhoto` | `sendPhoto(chatId: number, photo: string, options?): Promise` | Send a photo to a chat | | `createInvoiceLink` | `createInvoiceLink(params: CreateInvoiceLinkParams): Promise` | Create a Stars invoice link | | `answerPreCheckoutQuery` | `answerPreCheckoutQuery(queryId: string, ok: boolean, errorMessage?): Promise` | Respond to a pre-checkout query (must answer within 10 seconds) | Use `call()` for any Bot API method not covered by the convenience methods. ### Creating a Stars invoice ```typescript const invoiceUrl = await telegram.createInvoiceLink({ title: 'Premium', description: '30 days of premium', payload: JSON.stringify({ plan: 'premium' }), currency: 'XTR', prices: [{ label: 'Premium', amount: 100 }], }); ``` ### Answering a pre-checkout query ```typescript // In your bot webhook handler await telegram.answerPreCheckoutQuery(query.id, true); // Or reject with an error message await telegram.answerPreCheckoutQuery(query.id, false, 'Item out of stock'); ``` ## Full example A complete API route with auth, KV, and the Telegram API: ```typescript import { Hono } from 'hono'; import { requireUser, createKV, createTelegramApiClient } from '@tma.sh/sdk/server'; type Env = { Bindings: { KV: KVNamespace; BOT_TOKEN: string; OPENAI_API_KEY: string; }; }; const app = new Hono(); // Public health check app.get('/api/health', (c) => c.json({ status: 'ok' })); // Protected routes app.use('/api/*', requireUser()); app.get('/api/profile', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user'); const profile = await kv.get(`profile:${user.telegramId}`); return c.json({ user, profile }); }); app.post('/api/profile', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user'); const body = await c.req.json(); await kv.set(`profile:${user.telegramId}`, { bio: body.bio, updatedAt: Date.now(), }); return c.json({ ok: true }); }); export default app; ``` This file lives at `server/api/index.ts` in your project. TMA.sh detects it at build time, bundles it with esbuild, and deploys it to `{project}--api.tma.sh`. See [API Routes](/guides/api-routes/) for the full setup guide. --- # API Endpoints > SDK and platform API endpoint reference ## SDK API SDK endpoints are served by the platform API at `https://api.tma.sh`. The `@tma.sh/sdk` client handles these calls automatically. | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/sdk/v1/auth/validate` | Validate Telegram initData, returns JWT | | GET | `/.well-known/jwks.json` | Global JWKS keys for JWT verification | | PUT | `/sdk/v1/kv/:key` | Set a KV value | | GET | `/sdk/v1/kv/:key` | Get a KV value | | DELETE | `/sdk/v1/kv/:key` | Delete a KV value | | GET | `/sdk/v1/kv?prefix=` | List KV keys by prefix | | POST | `/sdk/v1/analytics` | Submit analytics events (`/sdk/v1/analytics/event` also accepted) | | POST | `/sdk/v1/payments/stars/invoice` | Create a Stars invoice link | ### Authentication **POST** `/sdk/v1/auth/validate` Validates the Telegram `initData` string and returns a signed JWT. The request body requires both `initData` and `projectId`. An optional `platform` field can be included to specify the client platform. The JWT can be verified against the global JWKS endpoint. ```bash curl -X POST https://api.tma.sh/sdk/v1/auth/validate \ -H "Content-Type: application/json" \ -d '{ "initData": "query_id=AAH...", "projectId": "your-project-id", "platform": "tma" }' ``` The `platform` field is optional and defaults to `"tma"` if omitted. **GET** `/.well-known/jwks.json` Returns the global public JSON Web Key Set for verifying JWTs issued by the `/sdk/v1/auth/validate` endpoint. This endpoint is served globally at `api.tma.sh` and is not per-project. Use this with any standard JWKS-compatible JWT library for server-side token verification. ```bash curl https://api.tma.sh/.well-known/jwks.json ``` ### KV Storage All KV endpoints require a valid JWT in the `Authorization` header. **PUT** `/sdk/v1/kv/:key` — Set a value. ```bash curl -X PUT https://api.tma.sh/sdk/v1/kv/user:preferences \ -H "Authorization: Bearer {jwt}" \ -H "Content-Type: application/json" \ -d '{"theme": "dark"}' ``` **GET** `/sdk/v1/kv/:key` — Retrieve a value. ```bash curl https://api.tma.sh/sdk/v1/kv/user:preferences \ -H "Authorization: Bearer {jwt}" ``` **DELETE** `/sdk/v1/kv/:key` — Delete a value. ```bash curl -X DELETE https://api.tma.sh/sdk/v1/kv/user:preferences \ -H "Authorization: Bearer {jwt}" ``` **GET** `/sdk/v1/kv?prefix=` — List keys matching a prefix. ```bash curl "https://api.tma.sh/sdk/v1/kv?prefix=user:" \ -H "Authorization: Bearer {jwt}" ``` ### Payments All payment endpoints require a valid JWT in the `Authorization` header. **POST** `/sdk/v1/payments/stars/invoice` — Create a Telegram Stars invoice link. The platform uses the project's stored bot token to create the invoice via the Telegram Bot API. No bot token is needed client-side. ```bash curl -X POST https://api.tma.sh/sdk/v1/payments/stars/invoice \ -H "Authorization: Bearer {jwt}" \ -H "Content-Type: application/json" \ -d '{ "title": "Premium Subscription", "description": "Access premium features for 30 days", "payload": "{\"plan\": \"premium\"}", "prices": [{"label": "Premium (30 days)", "amount": 100}] }' ``` Returns `{ "success": true, "data": { "invoiceUrl": "https://..." } }`. The `invoiceUrl` can be opened with `window.Telegram.WebApp.openInvoice()` or via the SDK's `tma.payments.stars.pay()` method. --- ## User API Routes Developers can define custom API routes by exporting a standard `fetch` handler from `server/api/index.ts`. Any framework that produces a Workers-compatible module works (Hono, itty-router, or plain fetch). These routes are deployed as a Cloudflare Worker and accessible at: ``` https://{project}--api.tma.sh/* https://pr{number}--{project}--api.tma.sh/* ``` Same-origin `/api/*` requests are also proxied on `{project}.tma.sh` and `pr{number}--{project}.tma.sh`. User-defined routes have access to project environment variables and KV bindings. See the [API Routes guide](/guides/api-routes) for setup details. --- ## GitHub Webhooks **POST** `/api/webhooks/github` Receives push events from GitHub to trigger auto-deployments. The request body is verified using HMAC-SHA256 signature validation via the `X-Hub-Signature-256` header. This endpoint is configured automatically when connecting a GitHub repository to a project. Manual setup is not required. --- # Limits & Quotas > Platform limits and resource quotas ## Deployment Limits | Resource | Limit | |----------|-------| | Static output (total) | 800 MB | | Code-like static file (`.html`, `.css`, `.js`, `.mjs`, `.cjs`, `.wasm`, `.map`, `.webmanifest`) | 10 MB per file | | Other static file | 25 MB per file | | API route bundle | 10 MB | | Bot handler bundle | 10 MB | | Command timeout (install/build in container) | 5 minutes per command | | Builder request timeout | 10 minutes | Builds fail if output validation or timeout limits are exceeded. --- ## Plan Quotas ### Core limits by tier | Resource | Free | Pro | Team | |----------|------|-----|------| | Projects | 1 | 1 | 5 | | Builds / month | 15 | 150 | 500 | | Requests / month (project) | 1,000,000 | 20,000,000 | 100,000,000 | | Campaigns | 0 | 5 | 100 | | Analytics access | No | Yes | Yes | | KV browser access | No | Yes | Yes | ### Managed DB (D1) limits by tier | Resource | Free | Pro | Team | |----------|------|-----|------| | Managed database | Not available | Available | Available | | Storage cap (per project) | 0 MB | 500 MB | 2,000 MB | | Row reads / day | 0 | 5,000,000 | 25,000,000 | | Row writes / day | 0 | 100,000 | 500,000 | | Reads / invocation | 0 | 10,000 | 50,000 | | Writes / invocation | 0 | 500 | 2,000 | | Analytics events / month | 0 | 5,000,000 | 50,000,000 | These values are enforced by platform tier config and usage/limit enforcement logic. --- ## SDK Endpoint Rate Limits Per-minute limits (tier-based): | Endpoint group | Free | Pro | Team | |----------------|------|-----|------| | `sdk-auth` | 60 | 200 | 500 | | `sdk-kv` | 120 | 500 | 2,000 | | `sdk-payments` | 30 | 100 | 300 | | `sdk-analytics` | 60 | 200 | 500 | | `webhook` | 30 | 30 | 30 | | Platform `/api/*` | 100 | 300 | 1,000 | --- ## KV Limits SDK KV route enforcement: | Resource | Limit | |----------|-------| | Max key length | 512 bytes | | Max value size | 128 KB (`131,072` bytes, JSON-serialized) | Cloudflare KV consistency behavior still applies (eventual consistency across regions). --- ## Runtime Notes User API routes run on Cloudflare Workers for Platforms. Runtime constraints like CPU/memory/body size/subrequest caps are inherited from Cloudflare's platform limits. --- ## Scheduled Jobs Background worker cron schedules: | Schedule | Job | |----------|-----| | `*/1 * * * *` | Outbox/domain event sweep | | `*/10 * * * *` | D1 usage reconciliation | | `0 * * * *` | Polar usage sync | | `30 0 * * *` | Analytics rollup | | `0 3 * * *` | Deployment + analytics cleanup | ### Deployment cleanup behavior - Active production deployment is never cleaned. - Non-active terminal deployments (`ready`/`failed`) retain the most recent 10. - Older terminal deployments are moved to `cleaned`. - Deployments already in `cleaned` are finalized to `purged` on later cleanup runs. - Preview deployments are marked `cleaned` immediately when staging PR is switched/closed. --- # LLM-Friendly Docs > Machine-readable documentation for AI assistants and LLMs TMA.sh provides machine-readable versions of this documentation following the [llms.txt standard](https://llmstxt.org/). These files are designed for consumption by AI assistants, coding tools, and large language models. ## Available files | File | Description | |------|-------------| | [`/llms.txt`](/llms.txt) | Index of all documentation pages with titles, descriptions, and links | | [`/llms-full.txt`](/llms-full.txt) | Complete documentation concatenated into a single file | ## Usage ### With AI coding assistants Point your AI assistant to the full documentation: ``` https://docs.tma.sh/llms-full.txt ``` This gives the model complete context about TMA.sh in a single request. ### With tools that support llms.txt Many AI tools automatically discover `/llms.txt` at the root of a documentation site. The index file links to individual pages and includes a reference to the full text version. ## Format Both files are plain text Markdown. They are regenerated on every docs build so they always reflect the latest documentation. - **llms.txt** lists every page with its title, URL, and description - **llms-full.txt** contains the full content of every page, separated by `---` dividers, ordered by section (Getting Started, Guides, Concepts, CLI, SDK, Reference) --- # Project Configuration > Configuration options for TMA.sh projects ## Local Configuration TMA.sh stores local project settings in `.tma/project.json` at the root of your repository. This file is created by `tma init` or `tma link`. `tma init` writes: ```json { "projectName": "my-app" } ``` `tma link` writes: ```json { "projectId": "11111111-2222-4333-8444-555555555555", "orgId": "66666666-7777-4888-8999-000000000000", "projectName": "my-app" } ``` Keep `.tma/project.json` in version control, but ignore local runtime artifacts: ```gitignore .tma/* !.tma/project.json ``` --- ## Dashboard Settings These settings are configurable from the TMA.sh dashboard or via the platform API. | Setting | Default | Description | |---------|---------|-------------| | `name` | -- | Project display name | | `slug` | -- | URL slug (used in URLs and API) | | `subdomain` | -- | Derived from slug (lowercase, special chars replaced with hyphens) | | `repoUrl` | -- | GitHub repository URL | | `branch` | `main` | Branch to deploy from | | `installCommand` | `npm install` | Dependency install command | | `buildCommand` | `npm run build` | Build command | | `outputDir` | `dist` | Build output directory | | `autoDeploy` | `true` | Auto-deploy on push to main | | `previewPrNumber` | `null` | Selected pull request number used as the staging preview target | The `slug` is used as a project identifier in URLs and the API, while the `subdomain` is a normalized version of the slug (lowercase, special characters replaced with hyphens). Both exist as separate database columns. Only one staging preview target can be selected at a time. Set `previewPrNumber` to `null` to disable preview deployments. Your project is served at `https://{subdomain}.tma.sh` for static assets and `https://{subdomain}--api.tma.sh` for API routes. --- ## Build Output Requirement TMA.sh deploys static SPA output. Your configured `outputDir` must contain an `index.html` file after the build command completes. --- ## Subdomain Restrictions There are over 60 reserved subdomains that cannot be used as project slugs. These span several categories including: - **Core platform**: `api`, `app`, `dashboard`, `admin`, `www` - **Environments**: `staging`, `preview`, `dev`, `test`, `demo`, `sandbox`, `internal` - **Auth/accounts**: `auth`, `login`, `signup`, `account`, `sso` - **Static/CDN**: `cdn`, `assets`, `static`, `media` - **Docs/support**: `docs`, `help`, `support`, `status`, `blog` - **Mail/DNS**: `mail`, `smtp`, `mx`, `ns1`, `ns2` - **Network/infra**: `proxy`, `vpn`, `wpad`, `git`, `svn`, `ssh`, `sftp` - **API protocols**: `graphql`, `ws`, `wss`, `rpc`, `grpc`, `webhook`, `webhooks` - **Observability**: `metrics`, `monitoring`, `logs`, `analytics`, `telemetry`, `health`, `healthcheck`, `ping` If you attempt to create a project with a reserved slug, the API will return an error. Choose a different name or prefix your slug (e.g., `my-app` instead of `app`).