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 trustworthyPython
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/todoThen 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.