# 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 a Hono-powered API deployed to the edge automatically. ## How it works ```bash # Install the CLI bun add -g @tma.sh/cli # Create a new project tma init my-app # Start developing cd my-app && tma dev # Deploy to production tma deploy ``` Simple key-value storage scoped per project. No database setup needed for common use cases. Every pull request gets its own deployment at `pr{number}--yourapp.tma.sh` with a dedicated bot. 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 **Vercel for Telegram Mini Apps** -- zero-config deployments with auth, payments, and storage 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** -- every pull request gets its own deployment at `pr{number}--yourapp.tma.sh` with a dedicated staging bot. - **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 a Hono-powered API deployed to `{project}--api.tma.sh`. ## Supported frameworks TMA.sh builds and deploys **static SPAs** only. The following frameworks are detected and supported for deployment: | 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 are supported for deployment (framework detection works automatically) but there is no Astro init template -- use `tma link` to connect an existing Astro project instead. 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 deployed Telegram Mini App in four commands: ```bash bun add -g @tma.sh/cli tma init my-app cd my-app && tma dev tma deploy ``` 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. **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; ``` 4. **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 ``` 5. **Test in Telegram.** Open your bot in Telegram, tap the menu button (bottom-left), and your app loads inside the Telegram client. Changes you make locally appear instantly via hot module replacement.
## 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 every push to `main`. 3. **Push a change to trigger a deploy.** Any push to `main` kicks off an automatic deployment. Your first deploy is already queued from the previous step -- check the deployment 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 ``` The bot's 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 configured install and build commands (defaults to `npm install` and `npm run build`). 3. **Upload assets** -- the build output is uploaded to the global CDN. 4. **Update bot URL** -- the Telegram bot's 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`. ## 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 powered by Hono. - **Bot handlers** -- optionally set up bot command and message handlers. - **Dependency installation** -- automatically runs `bun install` when scaffolding completes. After init finishes, your project is ready to develop: ```bash cd my-app tma dev ``` ## 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. ## 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": "proj_abc123", "orgId": "org_xyz789", "projectName": "my-app" } ``` This file identifies the project when running CLI commands. Commit it to your repository so that deployments work from CI and other machines. ## 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 Hono app in `server/api/index.ts`. At build time, TMA.sh detects this file, bundles it with esbuild, and deploys it to Cloudflare Workers for Platforms. Your API becomes available at `{project}--api.tma.sh/api/*`. ## Quick start Create a `server/api/index.ts` file in the root of your project: ```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. Your static SPA is served at `{project}.tma.sh` and your API at `{project}--api.tma.sh/api/*`. Same-origin `/api/*` requests on `{project}.tma.sh` are also proxied to the API worker, so you can use relative paths like `/api/health` from your client-side code. ## Available bindings API routes run on Cloudflare Workers and have access to the following bindings: - **KV** -- a per-project KV namespace, provisioned automatically - **Environment variables** -- secrets and configuration set via the dashboard or `tma env` Use TypeScript generics to get typed access to bindings: ```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; ``` ## External databases Per-project D1 is not available in the current release. For relational data, 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 ```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 Hono's 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 | | ---------------- | ---------------- | | Bundle size | 10 MB | | Request body | 100 MB | | Execution time | 30 seconds | | Subrequests | 50 per request | ## Route organization For larger APIs, use Hono's 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 | `proj_abc123` | | `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 use the TMA.sh JWT directly with Supabase by configuring a custom JWT secret. **Step 1:** In the TMA.sh dashboard, go to your project's Settings and copy the JWT signing secret. **Step 2:** In your Supabase dashboard, go to Settings > API and set the JWT secret to the same value. **Step 3:** Pass the JWT to the Supabase client: ```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 RLS policies now see the Telegram user as `auth.jwt().sub` const { data } = await supabase .from('profiles') .select('*') .eq('telegram_id', 'tg_123456789'); ``` This lets you use Supabase Row Level Security (RLS) with Telegram identities without managing a separate user table. ## 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 Add a `CNAME` record at your DNS provider pointing to `tma.sh`: | Type | Name | Value | TTL | | ------- | ------ | --------- | ---- | | `CNAME` | `app` | `tma.sh` | Auto | For an apex domain (e.g., `yourdomain.com` without a subdomain), some DNS providers require an `ALIAS` or `ANAME` record instead of `CNAME`. Check your provider's documentation. ### Step 3: Wait for verification TMA.sh checks for the DNS record automatically. Verification usually completes within a few minutes, but DNS propagation can take up to 48 hours 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/api/*`. 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. --- # 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 Variables can be scoped to specific environments: | Scope | Applied to | | -------------- | ------------------------------------------- | | **Production** | Production deployments (`tma deploy`) | | **Preview** | Preview deployments (pull request branches) | | **Development**| Local development (`tma dev`) | If a variable is set without a scope, it applies to all environments. Scoped variables override unscoped ones. Environment scoping is managed through the dashboard at **Settings > Environment Variables**, where you can set different values for each environment. The CLI always targets the production environment. ## Accessing variables in API routes Environment variables are available on the `c.env` object in your Hono API routes: ```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 During `tma dev`, environment variables scoped to `development` are loaded automatically. You can also use a `.env` file in your project root for local overrides: ```bash # .env (git-ignored by default) DATABASE_URL=postgres://localhost:5432/myapp STRIPE_SECRET_KEY=sk_test_... ``` The CLI loads `.env` values as a fallback when development-scoped variables are not set in the dashboard. ## 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. - Variables are **not logged** in build output or deployment logs. - Use the dashboard or CLI to **rotate secrets** without redeploying 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); }); ``` ## 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, { ttl: 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, connect to an external database from your API routes: - **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 handles the dev server and API routes locally. ## 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 (HMR) | | API server | 8787 | Miniflare (local Workers runtime) for API routes | ```bash tma dev ``` ``` SPA http://localhost:5173 API http://localhost:8787/api 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` exists, TMA.sh watches the file with esbuild and runs it locally using Miniflare (Cloudflare's local Workers simulator) on port 8787. The Vite dev server automatically proxies `/api/*` requests to Miniflare. ## Staging bot If your project already has a production bot serving live users, `tma dev` will warn you before updating its Web App URL: ``` WARNING: Bot @myapp_bot is in production with active users. Changing its Web App URL will affect all users. Create a staging bot instead? (recommended) [Y/n] ``` Selecting **Y** walks you through creating a staging bot via BotFather. The staging bot is saved to your project configuration and used for all future `tma dev` sessions. Your production bot remains untouched. You can also register a staging bot manually: ```bash tma bot register ``` The command prompts interactively for bot details. ## Local KV persistence KV data written during local development is persisted to disk at `.tma/kv-data/` in your project directory. This means your KV state survives restarts of `tma dev`. ``` my-app/ .tma/ kv-data/ # Local KV persistence config.json # Project configuration server/ api/ index.ts src/ ... ``` To reset local KV data, delete the `.tma/kv-data/` directory. ## Environment variables During local development, environment variables scoped to the `development` environment are loaded automatically. You can also place a `.env` file in your project root: ```bash # .env DATABASE_URL=postgres://localhost:5432/myapp BOT_TOKEN=123456:ABC-DEF ``` The `.env` file is used as a fallback when a variable is not set in the dashboard for the development environment. The `.env` file is git-ignored by default. 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 When you push code to your repository, TMA.sh automatically detects your framework, builds your app, and deploys the output. This page explains each step of that process. ## Framework detection TMA.sh inspects your `package.json` to determine how to build your project. Three frameworks are supported: | Framework | Detection | Build command | Output directory | |-----------|-----------|---------------|------------------| | Vite | `vite` in dependencies or devDependencies | `bun run build` | `dist/` | | Astro | `astro` in dependencies or devDependencies | `bun run build` | `dist/` | | Plain HTML | No framework detected | Static copy | `./` | Detection follows priority order: Vite is checked first, then Astro, then the fallback to plain HTML. If your project uses a different setup, you can override the install command, build command, and output directory in project settings. :::note TMA.sh hosts static Single Page Applications. Server-side rendering frameworks like Next.js, SvelteKit, and Nuxt are not supported. If you use these frameworks, configure them for static export. ::: ## Build steps The full pipeline from webhook to live deployment: ``` 1. GitHub webhook received 2. Deployment record created (status: queued) 3. Build job enqueued to Cloudflare Queue 4. Build container starts a. Clone repository at commit SHA b. Install dependencies (bun install) c. Run build command (bun run build) d. Validate output (index.html must exist) e. Upload assets to R2 f. Update KV routing tables g. Update Telegram bot menu URL 5. Deployment status: ready ``` Each step is recorded in the deployment's build log. If any step fails, the deployment is marked as `failed` and the previous production deployment continues serving traffic. ## Deployment status flow A deployment moves through a fixed set of statuses: ``` queued → building → deploying → ready ↘ failed Any status (ready, failed, etc.) → cleaned → purged ``` - **queued** -- Deployment record exists, waiting for a build container. - **building** -- Build container is running: installing dependencies, executing the build command. - **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** -- An old deployment beyond the retention window, marked for cleanup by the cron job. Applies to any deployment (including `ready` ones), not just failed builds. - **purged** -- R2 assets have been permanently deleted by the next cleanup cron run after being marked as `cleaned`. ## Build configuration Default build settings are determined by framework detection, but you can override them in project settings: ```json { "installCommand": "bun install", "buildCommand": "bun run build", "outputDirectory": "dist/" } ``` Common overrides include custom build commands (e.g., `bun run build:telegram`), different output directories, or additional install steps. ## 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/* ``` Server routes have access to KV storage and environment variables defined in your project's secrets. They do not have access to a per-project database in the current version. ```typescript // server/api/index.ts import { Hono } from "hono" const app = new Hono() app.get("/api/health", (c) => { return c.json({ status: "ok" }) }) export default app ``` ## 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 to R2 as the build progresses and are accessible from the dashboard. Each deployment retains its build log for debugging. Logs include: - Framework detection results - Dependency installation output - Build command output (stdout and stderr) - Asset upload summary (file count, total size) - Routing update confirmation - Timing information for each step ## 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 ``` ### 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 is a deployment ID pointing to the correct set of assets in R2. 4. The Worker fetches the requested file from R2 and returns it. ``` myapp.tma.sh/index.html → KV get "route:myapp" → "deploy_abc123" → R2 get "deploy_abc123/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" → "deploy_abc123" → R2 get "deploy_abc123/index.html" → 200 OK ``` Custom domains require a CNAME record pointing to `proxy.tma.sh`. TLS certificates are provisioned automatically through Cloudflare. ## 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-a1b2c3.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-x9y8z7.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 → R2 get "deploy_abc123/settings" → not found → Path does not match static extension allowlist → SPA fallback → R2 get "deploy_abc123/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`. You can tighten the policy through project settings if your use case allows it. ## 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" → "deploy_v2" After rollback: KV "route:myapp" → "deploy_v1" ``` This is a **sub-second operation** because it only writes a single KV entry. There is no re-upload, no rebuild, and no downtime. Global propagation of the KV update takes approximately 60 seconds due to Cloudflare's eventual consistency model. You can trigger a rollback from the dashboard or CLI by selecting any previous deployment from the project's deployment history. ## Deployment cleanup To manage storage costs, TMA.sh runs a two-stage daily cleanup process: **Stage 1: Mark as cleaned.** The cleanup cron identifies deployments beyond the retention window: - The **last 10 deployments** per project are always retained. - The **active production deployment** is never cleaned, regardless of age. - The **active preview deployments** (for open PRs) are never cleaned. - All other deployments (including old `ready` and `failed` ones) have their status set to `cleaned`. **Stage 2: Purge R2 assets.** On the next cron run, deployments already marked as `cleaned` have their R2 assets permanently deleted and their status set to `purged`. Cleaned and purged deployments retain their metadata (commit SHA, timestamp, build logs) but can no longer be used for rollback. If you need to return to a purged deployment, push the same commit again to trigger a fresh build. --- # 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 ``` The entire pipeline typically completes in under a minute for small projects. ## 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 by pull requests. Each PR gets its own URL at `pr{number}--{project}.tma.sh` and its own test bot. 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 + preview 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 pull requests. Old deployments are retained for instant rollback. ### Bots Each project has at least one Telegram bot for production. Preview environments get their own bots with `environment: 'preview'` so that 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 > Automatic deployments for pull requests Every pull request gets its own deployment with a unique URL and a dedicated Telegram bot. Preview environments let you test changes in a real Telegram context before merging to production. ## 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 ``` This URL is automatically posted as a comment on the pull request, along with a link to the deployment's build log. ## Lifecycle A preview environment follows the lifecycle of its pull request: ### 1. PR opened or updated When a pull request is opened (or a new commit is pushed to an existing PR), the GitHub webhook triggers a deployment with `type: 'preview'`. ``` PR opened → GitHub webhook → Deployment created (type: preview) ``` ### 2. Build The preview deployment goes through the same [build pipeline](/concepts/build-pipeline/) as production. Framework detection, dependency installation, build execution, and asset upload all work identically. ### 3. Bot assignment Each preview environment gets its own Telegram bot with `environment: 'preview'`. This bot is separate from the production bot, so testing interactions in the preview never affects your live users. The preview bot's menu button URL is set to the preview deployment URL (`pr{number}--{project}.tma.sh`), making it immediately testable in Telegram. ### 4. PR updated When new commits are pushed to the PR branch, the preview deployment is rebuilt. The URL stays the same (`pr{number}--{project}.tma.sh`), but it now serves the updated build. The same preview bot is reused. ### 5. 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. When you create a secret, you choose whether it applies to `production`, `preview`, `development`, or a combination of these. 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 Test different approaches on separate branches. Each PR gets its own isolated environment, so multiple experiments can run in parallel without interference. ### Bot interaction testing Since each preview has its own bot, you can test bot commands, inline queries, and webhook handlers without worrying about conflicts with the production bot's state. ## Comparison with production | Aspect | Production | Preview | |--------|-----------|---------| | URL | `{project}.tma.sh` | `pr{number}--{project}.tma.sh` | | Trigger | Push to default branch | Pull request opened/updated | | Bot | Production bot | Dedicated preview bot | | Secrets | Production-scoped | Preview-scoped | | Custom domains | Supported | Not supported | | Cleanup | Retained (last 10) | Removed when PR is closed | | Rollback | Supported | Not applicable | ## Disabling previews If you don't need preview environments, you can disable them in project settings. When disabled, pull requests will not trigger deployments. You can re-enable them at any time. --- # 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` or `tma --help` for usage details on any command. ## Project configuration When you run `tma init` or `tma link`, the CLI creates a `.tma/project.json` file in your project root: ```json { "projectId": "proj_abc123", "orgId": "org_xyz789", "projectName": "my-app" } ``` This file tells the CLI which TMA.sh project this directory belongs to. Commit it to version control so your team and CI pipelines can resolve the project automatically. 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 powered by Hono. 3. **Bot handlers** -- optionally include a `bot/index.ts` file for Telegram bot command handlers. 4. **Dependency installation** -- runs `bun install` automatically after scaffolding. The command creates the project directory, writes all template files, and generates `.tma/project.json` to link the directory to your TMA.sh project. ``` 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 ``` ## `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. After selection, it writes `.tma/project.json` to the current directory. ## `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. 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... Building (Vite detected)... Deployment live at https://my-app.tma.sh ``` ### Options | Flag | Description | |------|-------------| | `--preview` | Trigger a preview deployment instead of production | ## `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. They are injected at build time and available as `process.env.*` in API routes. Sensitive values (tokens, keys) are masked in build logs. ## `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 dpl_abc123 # 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 the build output, including framework detection, build commands, asset upload progress, and any 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` or `staging`). | | `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()` takes a config object with your `projectId`. On any `*.tma.sh` deployment, the SDK uses this to connect to the correct project backend. ## 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/) -- Hono handlers that run on the edge. It includes auth middleware, a typed KV wrapper, and `initData` validation utilities. ## 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, typed KV bindings, and `initData` validation for your Hono API routes. Everything you need to build authenticated server-side logic. 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 with your project's 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 | `proj_abc123` | | `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 TMA.sh JWTs work directly with Supabase Row Level Security. Set your project's JWT secret in the TMA.sh dashboard (Settings > Auth > JWT Secret), then pass the token to the Supabase client: ```typescript import { createClient } from '@supabase/supabase-js'; const supabase = createClient( 'https://your-project.supabase.co', 'your-anon-key', { global: { headers: { Authorization: `Bearer ${jwt}` }, }, } ); // RLS policies can use auth.uid() which returns 'tg_123456789' const { data } = await supabase .from('profiles') .select('*') .eq('user_id', user.telegramId); ``` This pattern works with any backend that supports JWT verification -- Firebase, Turso, or your own service. ## 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 API routes deployed on TMA.sh, use the `requireUser()` middleware instead of manual JWT verification. It extracts the Bearer token, verifies it via JWKS, and injects the user into the Hono context. See [Server Helpers](/sdk/server/) for usage. --- # 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, connect an external database like Supabase or Turso instead: - **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 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/) -- Hono handlers that run on the edge alongside your static app. These helpers handle auth middleware, typed KV access, `initData` validation, and Telegram Bot API calls. ```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 the project's 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<{ keys: { name: string }[], list_complete: boolean }>` | List keys by 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()`. ## `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 All SDK endpoints are served from your project's API subdomain. The `@tma.sh/sdk` client handles these automatically — direct API calls are only needed for custom integrations. | 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 | | 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://{project}--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://{project}--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://{project}--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://{project}--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://{project}--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 Hono app from `server/api/index.ts`. These routes are deployed as a Cloudflare Worker and accessible at: ``` https://{project}--api.tma.sh/api/* ``` 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 asset bundle | No hard limit (R2) | | API route bundle | 10 MB compressed | | Build timeout | 10 minutes | | Deployments retained | Last 10 per project | Static assets are stored in Cloudflare R2 with no enforced size cap. API route bundles are deployed to Workers for Platforms and must stay within the compressed size limit. --- ## API Route Runtime Limits API routes run on Cloudflare Workers. The following per-request limits apply: | Resource | Limit | |----------|-------| | CPU time per request | 30 seconds | | Memory | 128 MB | | Subrequests per request | 1,000 | | Request body size | 100 MB | These limits are inherited from the Cloudflare Workers platform. Requests that exceed CPU time or memory will be terminated. --- ## KV Storage Limits | Resource | Limit | |----------|-------| | Max value size | 128 KB | | Max key size | 512 bytes | | Consistency | Eventually consistent (~60s) | KV is backed by Cloudflare KV. The platform enforces a 128 KB value size limit, which is lower than the raw Cloudflare KV limit. Writes propagate globally within approximately 60 seconds. For use cases that require strong consistency, consider using an external database. --- ## Platform Limits Included resources per plan (Free tier shown): | Resource | Free Tier | |----------|-----------| | D1 reads | 25 billion/month | | D1 writes | 50 million/month | | KV reads | 10 million/month | | KV writes | 1 million/month | | R2 storage | 10 GB free, then $0.015/GB | These limits apply to internal platform operations (project metadata, deployment records, etc.) and SDK-level KV usage combined. Usage beyond the free tier is billed according to your plan. --- ## CDN Cache Behavior Static assets are served through Cloudflare's CDN with the following cache rules: | Content Type | Cache Duration | |-------------|---------------| | HTML files | `no-cache, must-revalidate` (allows caching with revalidation) | | Hashed assets (e.g., `app-a1b2c3.js`) | 1 year, immutable | | Other static files | 24 hours | HTML files are served with the `no-cache, must-revalidate` cache header, which allows browsers and CDN edges to store the response but requires revalidation before serving it. This ensures users always receive the latest version after a deployment while still benefiting from conditional requests (304 Not Modified). Hashed assets use content-based filenames, so they are safe to cache indefinitely. All other static files (images, fonts, etc.) are cached for 24 hours. --- ## Scheduled Jobs Two daily cron jobs maintain the platform: | Schedule | Job | Description | |----------|-----|-------------| | `30 0 * * *` (00:30 UTC) | Analytics rollup | Aggregates raw analytics data into rollup tables | | `0 3 * * *` (03:00 UTC) | Deployment cleanup | Removes old deployments beyond the retention window | ### Deployment Cleanup The cleanup cron removes old deployments with the following rules: - Keeps the **last 10 deployments** per project. - The **active deployment is never deleted**, even if it falls outside the retention window. - Associated R2 assets and Worker scripts are cleaned up alongside the deployment record. ### Deployment Lifecycle Deployments progress through the following statuses: `queued` --> `building` --> `deploying` --> `active` --> `superseded` --> `cleaned` --> `purged` The `cleaned` status indicates the deployment record has been marked for removal. Once the associated R2 assets are fully deleted, the deployment transitions to `purged`. --- # 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`. ```json { "projectName": "my-app" } ``` Add `.tma/` to your `.gitignore` — it contains local state only. --- ## 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 | 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. Your project is served at `https://{subdomain}.tma.sh` for static assets and `https://{subdomain}--api.tma.sh` for API routes. --- ## Framework Detection When you connect a repository, TMA.sh inspects your `package.json` to detect the framework in use and pre-fill build settings. | Framework | Detection Signal | Default Build | Default Output | |-----------|-----------------|---------------|----------------| | Vite | `vite` in dependencies or devDependencies | `npm run build` | `dist/` | | Astro | `astro` in dependencies or devDependencies | `npm run build` | `dist/` | | Plain HTML | No framework detected | (static copy) | `./` | Detection runs on project creation and after each successful build. You can override the detected defaults in the dashboard at any time. ### Not Supported SSR frameworks are not supported. TMA.sh is a static SPA hosting platform — server-side rendering is outside its scope. The following frameworks will be rejected during detection: - SvelteKit (`@sveltejs/kit`) - Next.js (`next`) - Nuxt (`nuxt`) If your project uses one of these frameworks, configure it to produce a static export (e.g., `output: 'static'` in Astro) and set the build settings manually. --- ## 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`, `canary` - **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**: `gateway`, `proxy`, `edge`, `node`, `cluster` - **API protocols**: `graphql`, `grpc`, `ws`, `websocket` - **Observability**: `metrics`, `logs`, `traces`, `monitor` 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`).