Mini-app authentication#

A mini-app runs in the user's browser, so nothing it says about identity can be trusted on its own. Mafold solves this the same way the Telegram Bot API does: on launch it hands your page a signed token describing the user, and your backend verifies the signature with a secret only you and Mafold share.

Platform rule: Mafold identity is the only identity

If your mini-app identifies users, Mafold.auth() / initData verification is the only permitted way. Inside Mafold, apps must not show their own username/password signup or third-party login (Google, email magic links, …). Users are already signed in — an app that asks them to log in again is broken UX, and third-party OAuth flows routinely fail inside embedded webviews anyway. Apps that ship their own login are rejected from the registry. (Apps that don't need identity at all are fine — identity is optional; rolling your own is not.)

The two identities#

  • Mafold.initDataUnsafe — the decoded claims, ready to render. Use it for the greeting and the avatar. Never trust it for anything that matters — it is not verified.
  • Mafold.initData — the raw signed JWT. Send it to your backend; verify it there; then act on the identity inside.
// client
await fetch("/api/session", {
  method: "POST",
  headers: { authorization: `Bearer ${Mafold.initData}` },
});

The token#

initData is a JWT signed with HS256 using your app's registration secret (the analogue of a bot token — you get it once when you register the app).

Claims:

{
  "iss": "mafold",
  "app": "you/todo",
  "user": { "username": "ada", "display_name": "Ada", "avatar_url": "…" },
  "conv": "<conversation id, if launched in one>",
  "iat": 1739500000,
  "exp": 1739503600
}

It expires one hour after issue (exp = iat + 3600). For long-lived sessions, call Mafold.refreshInitData() on the client to mint a fresh one before your next backend call.

Verify on your backend#

Verify the signature, the issuer, and the expiry with your app secret. Reject anything that doesn't check out.

Verify server-side, always

Do the verification on your server, never in the browser. The whole point is that the browser can't be trusted — only your backend holds the secret.

Node

import jwt from "jsonwebtoken";

const claims = jwt.verify(initData, APP_SECRET, {
  algorithms: ["HS256"],
  issuer: "mafold",
});
// claims.user.username is now trustworthy

Python

import jwt

claims = jwt.decode(
    init_data, APP_SECRET,
    algorithms=["HS256"], issuer="mafold",
)

The library's verify / decode enforces both the signature and exp; passing issuer also rejects tokens that weren't minted for Mafold.

If your secret leaks#

Rotate it — the old secret stops verifying immediately:

mafold apps rotate-secret you/todo

Then redeploy your backend with the new value. (Details in Publishing.)

Standalone mode#

When your app is opened directly (not inside a chat), there is no launch token. Call Mafold.auth() — it runs Login with Mafold (OAuth authorization code + PKCE) and returns the user after they approve. Your backend then trusts the session your own login establishes; chat.* features stay disabled because there is no surrounding conversation.