OTP (one-time passcode) authentication lets users sign in by entering a short numeric code sent to their email address. No password is stored or required.
Enabling OTP Auth#
Add with OtpAuth to your auth table class. No additional columns are added — OTP codes are transient and not persisted to the schema:
final class UserTable extends AuthTable<User>
with OtpAuth {
// or combined with PasswordAuth:
// with PasswordAuth, OtpAuth
}
final users = authTable('users', UserTable.new);
The OTP Flow#
Step 1 — Request a code:
POST /auth
{
"type": "sendOtp",
"table": "users",
"email": "alice@example.com",
"metadata": { "name": "Alice" }
}
Returns 200 OK with an empty body. Zonai generates a 6-digit code, stores only its hash, and sends it via the
otp_code email template. The optional metadata object is kept with the code and becomes the new row's extra fields if this turns out to be a sign-up.
There is no separate sign-up call: if no account exists for the email and canSignUp allows it, verifying the code creates one.
beforeSignUp runs both here and again at verify — see Declining a sign-up.
One code per address per minute: a second request inside the minute answers 429. Requesting a new code invalidates the previous one.
Step 2 — Verify the code:
POST /auth/confirm
{
"type": "verifyOtp",
"email": "alice@example.com",
"code": "123456"
}
On a valid code: canSignIn (or canSignUp for a new account) in auth row rules is evaluated, the
onSignIn (or onSignUp) extension fires, and the response includes the user row and an
accessToken.
On an invalid or expired code: 401 Unauthorized.
A code is valid for 10 minutes and allows 3 verification attempts; after that the user must request a new one. Both values are fixed — there is no per-table configuration for them. Sending a code goes through POST /auth, so it is throttled by authenticatePolicy() (the declared sendOtpPolicy() is never consulted), plus a fixed one-send-per-address-per-minute limit. See Auth Rate Limits.
Using OTP Alongside Password Auth#
A table can use both PasswordAuth and OtpAuth at the same time:
final class UserTable extends AuthTable<User>
with PasswordAuth, OtpAuth {
// ...
}
Users can sign in via either method — both lead to the same account and the same kind of session. OTP is an alternative way in, not a second factor on top of the password. OTP sign-in is also unaffected by a forced password reset.