TL;DR: In the console (lingcode.dev/backends β Authentication) turn on Require MFA for all signed-in users. In your app, after sign-in call client.auth.enrollMfa() (show the returned otpauthUrl as a QR for an authenticator app), then client.auth.verifyMfa({ code }). Once required, the data API rejects un-verified sessions until they pass a TOTP code. Access tokens are short-lived and the SDK refreshes them automatically.
A password alone is one stolen credential away from a breach. MFA adds a second factor β a rotating 6-digit code from an authenticator app (TOTP) β so a leaked password isn't enough. On most stacks that means bolting on an auth vendor; on a LingCode Cloud backend the users system already supports it, and turning it on is a toggle plus two SDK calls.
This builds on a connected managed backend whose users sign in with email/password, magic link, OTP, or social login. MFA layers a TOTP second factor on top, and the backend tracks an assurance level per session: aal1 after a normal sign-in, aal2 once a TOTP code is verified.
In lingcode.dev/backends β Authentication, toggle Require MFA for all signed-in users. With it on, the data API rejects any request from a user session that hasn't reached aal2, returning a mfa_required error β your app catches that and walks the user through enrollment/verification. (Leave it off and MFA is opt-in: users can still enroll, but it isn't enforced.) The public anon key is unaffected β this gates user sessions, not anonymous reads governed by your RLS.
After the user signs in, offer "Enable two-factor." Enrolling returns a shared secret and an otpauth:// URL β render that URL as a QR code (any small QR library) so the user can add it to Google Authenticator, 1Password, etc.:
const { data } = await client.auth.enrollMfa();
// data.otpauthUrl β render as a QR; data.secret β manual-entry fallback
Ask for the 6-digit code the app shows and verify it. The first successful verify completes enrollment and returns an aal2 session, which the SDK stores:
const { error } = await client.auth.verifyMfa({ code: userTypedCode });
// no error β session is now aal2; data calls succeed
On a later visit (or after a token refresh drops back to aal1), a protected call returns mfa_required; call client.auth.challengeMfa() to see the user's verified factors, prompt for a code, and verifyMfa again.
Sessions use a short-lived access token plus a rotating refresh token. The SDK refreshes transparently when the access token expires β and if a refresh token is ever replayed (a sign of theft), the whole token family is revoked. You don't manage any of this; it's why a user stays signed in without you shipping a long-lived token to the browser.