# 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
{/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`).