Hosting & CDN
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
Section titled “Asset serving”Every request to a TMA.sh-hosted app follows the same path:
Browser → Cloudflare Worker → KV lookup → R2 fetch → ResponseSubdomain routing
Section titled “Subdomain routing”Each project gets a subdomain at {project}.tma.sh. When a request arrives:
- The Worker extracts the subdomain from the hostname.
- It performs a KV lookup for the key
route:{subdomain}. - The value is a deployment ID pointing to the correct set of assets in R2.
- 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 OKCustom domains
Section titled “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 OKCustom domains require a CNAME record pointing to proxy.tma.sh. TLS certificates are provisioned automatically through Cloudflare.
Cache headers
Section titled “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
Section titled “How it works in practice”When you deploy a new version:
- New hashed asset files are uploaded to R2 (e.g.,
app-x9y8z7.js). - A new
index.htmlis uploaded that references these new files. - The KV routing pointer is updated to the new deployment.
- The next request for
index.htmlgets the new version (no-cache), which loads the new hashed assets. - Old hashed assets remain in R2 until the deployment is cleaned up, so in-flight requests are not broken.
SPA fallback
Section titled “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 OKFiles with known static extensions (.js, .css, .png, etc.) that don’t exist in R2 return a 404 normally.
Security headers
Section titled “Security headers”Every response includes security headers configured for the Telegram Mini App environment:
Content Security Policy
Section titled “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
Section titled “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
Section titled “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
Section titled “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
readyandfailedones) have their status set tocleaned.
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.