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 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.
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-embedOnly 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.