Passkeys or OTP?
How passkeys work, why they cannot be phished like an OTP, which devices support them, and how to add them next to the login you already have.
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.03 to Rs 0.45 | 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: registration token from your server, passkey made in the browser
import { passkeys } from "@signops/web";
await passkeys.register({ user_id: "usr_8f21c4", 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.
SignOps Passkeys runs the server side on your own domain, with 10,000 sign-ins a month free and no limit on Starter, and email OTP on the same account for recovery.
Sources: passkeys.dev, Android: Credential Manager, W3C: Web Authentication.