An OTP proves you can read a code sent to you. A passkey proves you hold a key made for this one website, unlocked by your fingerprint or screen lock. That difference is why passkeys are replacing codes for sign-in.
How a passkey works
- Registration. The phone or laptop makes a key pair for your domain. The private key stays on the device or in the user's password manager. Your server stores the public key.
- Sign-in. Your server sends a random challenge. The device asks for the fingerprint or screen lock, signs the challenge with the private key, and your server checks the signature with the public key.
The browser only lets a passkey answer the domain it was made for. A look-alike phishing site gets nothing.
Passkeys vs OTP
| SMS or email OTP | Passkey | |
|---|---|---|
| What the user does | Waits for a code, types it | One touch or glance |
| Can be phished | Yes | No |
| SIM swap risk | Yes for SMS | No |
| Cost per sign-in | Rs 0.025 to Rs 0.25 | Nothing to send |
| Works offline | No | Yes, on the device |
| Needs a new device to be set up | No | Yes, once per device or synced account |
Where OTP still fits
- Proving a new email or phone number at signup.
- Recovery when someone loses every device.
- Users on old devices without passkey support.
So the usual design is passkeys for sign-in, email OTP for signup and recovery.
Adding passkeys
The browser API is WebAuthn (navigator.credentials.create and .get). The work is on your server: making challenges, storing credentials and checking every response correctly. A minimal flow:
// Browser, with SignOps: your server makes a registration token for the signed-in user,
// and the browser makes the passkey with it
import { passkeys } from "@signops/web";
await passkeys.register({ token: registrationToken });
// Later: sign in, with the passkey offered in the email field (autofill)
const { signops_token } = await passkeys.signIn({ autofill: true });
// your server confirms signops_token before starting a session
On Android, passkeys go through Credential Manager; on iOS through AuthenticationServices. Flutter and React Native packages wrap both. To see what a browser and device support, open the passkey support checker on it.
SignOps Passkeys runs the server side on your own domain, with 1,000 sign-ins a month free and 50,000 on Starter, and email OTP on the same account for recovery.
FAQ
Are passkeys better than OTP?
For sign-in, yes. A passkey only answers the website it was made for, so it cannot be phished, and there is no code to send, read or intercept. OTP is still useful to verify a new email or phone, and to recover an account when the passkey is lost.
Which phones support passkeys?
Android 9 and later, iOS 16 and later, and current Chrome, Safari, Edge and Firefox. Synced passkeys are backed up by Google Password Manager, iCloud Keychain or password managers such as 1Password.
Can I add passkeys to Flutter or React Native?
Yes. On Android they go through Credential Manager and on iOS through AuthenticationServices; Flutter and React Native packages wrap both, and your server must check the WebAuthn responses.
What happens if a user loses their phone?
Synced passkeys come back on the new phone through the same Google or Apple account. If not, let them in with an email code and ask them to make a new passkey.
First published on signops.dev.
Sources: passkeys.dev, Android: Credential Manager, W3C: Web Authentication.
Top comments (0)