Skip to content

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.

Every request to a TMA.sh-hosted app follows the same path:

Browser → Cloudflare Worker → KV lookup → R2 fetch → Response

Each project gets a subdomain at {project}.tma.sh. When a request arrives:

  1. The Worker extracts the subdomain from the hostname.
  2. It performs a KV lookup for the key route:{subdomain}.
  3. The value contains routing metadata (including deployment ID and project/org IDs).
  4. The Worker fetches the requested file from R2 and returns it.
myapp.tma.sh/index.html
→ KV get "route:myapp" → { deploymentId: "...", ... }
→ R2 get "deployments/{deploymentId}/index.html"
→ 200 OK

Custom domains work the same way, with a different KV key pattern:

app.example.com/index.html
→ KV get "route:custom:app.example.com" → { deploymentId: "...", ... }
→ R2 get "deployments/{deploymentId}/index.html"
→ 200 OK

For subdomains, use CNAME to {project}.tma.sh. For apex/root domains, use ALIAS/ANAME flattening to tma.sh. TLS certificates are provisioned automatically through Cloudflare after verification.

TMA.sh sets cache headers based on the type of file being served:

File typeCache-ControlRationale
HTML filesno-cache, must-revalidateAlways serve the latest version
Hashed assets (e.g., app.a1b2c3d4.js)public, max-age=31536000, immutableContent-addressed, safe to cache indefinitely
Other static filespublic, max-age=86400Cache 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.

When you deploy a new version:

  1. New hashed asset files are uploaded to R2 (e.g., app.a1b2c3d4.js).
  2. A new index.html is uploaded that references these new files.
  3. The KV routing pointer is updated to the new deployment.
  4. The next request for index.html gets the new version (no-cache), which loads the new hashed assets.
  5. Old hashed assets remain in R2 until the deployment is cleaned up, so in-flight requests are not broken.

Since TMA.sh hosts Single Page Applications, it implements a fallback rule for client-side routing:

If the requested path does not match a known static file extension and the file does not exist in R2, serve index.html instead.

A hardcoded allowlist of static file extensions (e.g., .js, .css, .png, .svg, .woff2, .ico, etc.) is used to determine whether a request is for a static asset. Only requests matching these specific extensions bypass the SPA fallback and return a 404 if the file is not found. Requests for paths not matching the allowlist (including extensionless paths like /settings) are served index.html, allowing your client-side router (React Router, Vue Router, Svelte routing, etc.) to handle navigation.

myapp.tma.sh/settings
→ KV get "route:myapp" → { deploymentId: "<deployment-id>", ... }
→ R2 get "deployments/<deployment-id>/settings" → not found
→ Path does not match static extension allowlist → SPA fallback
→ R2 get "deployments/<deployment-id>/index.html"
→ 200 OK

Files with known static extensions (.js, .css, .png, etc.) that don’t exist in R2 return a 404 normally.

Every response includes security headers configured for the Telegram Mini App environment:

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;
HeaderValuePurpose
X-Frame-OptionsALLOW-FROM https://web.telegram.orgPermit embedding in Telegram’s WebView
X-Content-Type-OptionsnosniffPrevent MIME type sniffing

The CSP is intentionally permissive for script-src (allowing unsafe-inline and unsafe-eval) because many Telegram Mini App SDKs and frameworks require inline scripts. The frame-ancestors directive restricts embedding to Telegram’s web client and t.me.

Rollback is an instant operation. Since each deployment is an immutable set of files in R2 with its own deployment ID, rolling back means updating a single KV pointer:

Before rollback:
KV "route:myapp" → { deploymentId: "<current-deployment-id>", ... }
After rollback:
KV "route:myapp" → { deploymentId: "<previous-deployment-id>", ... }

This operation only updates routing metadata in KV. There is no rebuild and no asset re-upload. The route update then propagates globally through Cloudflare KV replication.

Rollback is available through the project dashboard by selecting a previous deployment.

To manage storage costs, TMA.sh runs a two-stage daily cleanup process:

Stage 1: Retention cleanup. The daily cleanup job evaluates deployment history:

  • The active production deployment is never cleaned.
  • Among non-active terminal deployments (ready/failed), the most recent 10 are retained.
  • Older terminal deployments are cleaned and moved to cleaned.

Stage 2: Purge cleaned records. On subsequent cleanup runs, deployments already in cleaned are finalized to purged.

Preview deployments are also marked cleaned immediately when the staging PR is changed/closed. Cleaned/purged deployments keep metadata (commit, timestamps, logs) but are not rollback targets.