MafoldGet Started
DocsAuthentication
Apps & mini-apps

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.

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.

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.

The embedded door

Mafold.auth() sends the visitor to mafold.com and brings them back. If Mafold is the only way into your app — which it is for most apps built on this kit — you probably want the login to happen without anyone leaving the page:

<div id="signin"></div>
<script src="https://mafold.com/js/mafold-webapp@0.5.0.js"></script>
<script>
  Mafold.mountSignIn("#signin", { appId: "acme/board" }).then(boot);
</script>

That frames Mafold's real door — handle, then a passkey or a password — inside your layout, and resolves with the user once they are through. Same OAuth flow as auth(): a one-time code crosses back over postMessage, and the SDK exchanges it with the PKCE verifier your page never let out.

Turn it on when you register the app. Until you do, mafold.com refuses to be framed by anybody and your frame comes up empty:

mafold apps register acme/board --url https://app.example.com --auth-embed

Only that URL's origin is allowed to frame the door — the same origin that is already the only place an authorization code may land.

What the visitor sees the second time

A cross-origin frame gets its own partitioned storage in every current browser, so the door cannot see a mafold.com session made anywhere else — it has to ask. Once they sign in through your frame, that session is remembered for your site, so the second visit is instant. This is a browser rule, not a Mafold choice: any embedded login works this way, including the ones you have used on other sites.

Mafold.mountSignIn() resolves immediately, drawing nothing, when a login is already remembered — so calling it on every page load is the intended usage.