Loading...
Loading...
Bi-weekly engineering deep dives on auth, notifications, and developer infrastructure. No spam.
Sign Up for UpdatesPublished by Zyphr
The "Forgot Password" link is where your conversion rate goes to die. Every time a user lands on that screen, you've already lost. They have to leave your app, open their email, wait for a delivery service that might be lagging, click a link, and then try to remember a new password that meets your arbitrary character requirements. It's a cycle of friction that costs you users and revenue. Traditional password-based authentication is a relic that forces users to carry a heavy cognitive load while exposing your platform to credential stuffing. This passkey authentication tutorial provides a roadmap to replacing that fragile system with hardware-backed, biometric security.
Passkeys, built on the WebAuthn standard, fundamentally change the relationship between a user and their digital identity. Instead of typing a shared secret, your user touches a sensor or looks at a camera. The problem is that implementing WebAuthn is notoriously difficult. It involves complex challenge-response cycles, CBOR encoding, and managing public key credentials. Most developers end up spending weeks building "glue code" to connect their auth provider to their messaging provider just to send a simple "Welcome" email after a successful registration. Zyphr eliminates this overhead by treating identity and communication as a single, integrated workflow.
WebAuthn isn't just a fancy login button. It's a shift from shared secrets to asymmetric cryptography. When a user registers a passkey, their device—known as the authenticator—generates a public/private key pair. The private key never leaves the device's secure enclave (the same hardware that protects Apple Pay or Android biometrics). Your server only ever sees the public key.
The mechanics follow a strict ceremony:
This architecture provides built-in phishing resistance. Because the browser enforces that the origin matches the one associated with the passkey, a user cannot accidentally use their passkey on a malicious lookalike site. It also removes the risk of database leaks. If your database is breached, the attacker only gets public keys, which are useless without the physical device and the user's biometric unlock.
The difficulty lies in the implementation details. You have to handle different browser capabilities, manage various attestation formats, and handle the transition from binary buffers to JSON-friendly formats. Most teams get bogged down in the complex auth features of the WebAuthn spec rather than building their product. Zyphr abstracts this entire handshake into a few clean methods.
To begin, you'll need the Zyphr SDK. We've designed it to be the only dependency you need for both identity and notification management.
Install the package via npm:
npm install @zyphr/sdk
Initialize the client on your backend. You'll need your API key from the Zyphr dashboard. You also need to define your Relying Party (RP) ID—this is typically your top-level domain.
import { Zyphr } from '@zyphr/sdk';
const zyphr = new Zyphr(process.env.ZYPHR_API_KEY);
// Configuration for WebAuthn
const authConfig = {
rpName: "Your SaaS Platform",
rpId: "saas-platform.com",
origin: "https://app.saas-platform.com",
};
In the Zyphr dashboard, enable Passkeys under the Authentication settings. This is where you configure your allowed origins to prevent unauthorized domains from initiating auth requests. Unlike other providers that treat messaging as an afterthought, Zyphr lets you link these auth events directly to subscriber profiles from the start.
The registration flow is where you convert a visitor into a verified user. Using the Zyphr SDK, you don't need to manually create challenges or verify signatures. The SDK handles the binary exchange with the browser's Credential Management API.
This function triggers the native browser prompt. Upon success, it creates a unified Subscriber record in Zyphr.
import { zyphr } from './zyphr-client';
async function handleRegisterPasskey(email: string) {
try {
// Start the registration process
// This triggers the native FaceID/TouchID prompt
const credential = await zyphr.auth.passkeys.register({
email: email,
userName: email.split('@')[0],
displayName: email,
requireResidentKey: true,
userVerification: "required"
});
console.log("Registration successful for user:", credential.id);
// The user is now logged in and a 'subscriber' is created in Zyphr
// with this passkey linked to their profile.
} catch (error) {
if (error.name === 'NotAllowedError') {
console.warn("User dismissed the biometric prompt.");
} else {
console.error("Passkey registration failed:", error);
}
}
}
Login is more efficient than traditional methods. Since the user's public key is already associated with their Subscriber ID in your project, the authenticate method identifies the correct user and verifies their identity in one step.
async function handleLogin() {
try {
// Triggers the browser to look for available passkeys
const session = await zyphr.auth.passkeys.authenticate();
// Store the session token locally
localStorage.setItem('zyphr_session', session.token);
window.location.href = '/dashboard';
} catch (error) {
console.error("Login failed:", error);
}
}
If you consult the passkey documentation, you'll see these methods return a full session object, including a JWT with configurable expiry. You don't have to manage session stores or refresh tokens manually unless you want to customize the behavior.
The real value of an integrated platform is what happens after the user logs in. In a traditional stack, you'd have to write a webhook listener or a background job to detect the new user, then call a third-party API like SendGrid or Twilio to send a welcome message. This creates a distributed system that you have to maintain and monitor.
In Zyphr, the moment passkey.register succeeds, a subscriber.created event fires. You can map this event to a multi-channel template in the visual editor. You can configure a "Welcome" flow that performs multiple actions:
The subscriber preference object is managed automatically:
{
"subscriber_id": "sub_01HGP283...",
"channels": {
"email": { "enabled": true, "address": "dev@example.com" },
"push": { "enabled": true, "tokens": ["fcm_token_xyz"] },
"in_app": { "enabled": true }
},
"preferences": {
"marketing": false,
"transactional": true
}
}
Because identity and messaging share the same data layer, there is zero latency. You don't have to worry about a "Right to be Forgotten" request hitting your auth database but leaving the user's email in your messaging tool. Zyphr treats them as a single entity.
One of the most powerful aspects of modern passkeys is "Conditional UI," often called Autofill. This allows the browser to suggest passkeys directly in the username field before the user even clicks a "Login" button.
To implement this, you add autocomplete="username webauthn" to your input field. When the page loads, you call the Zyphr authentication method with the conditional flag:
// Call this on page load to enable autofill suggestions
zyphr.auth.passkeys.authenticate({
mediation: 'conditional'
}).then(session => {
if (session) {
localStorage.setItem('zyphr_session', session.token);
window.location.href = '/dashboard';
}
});
This removes the need for the user to even remember which email address they used. They click the field, select their biometric profile, and they're in. It's the fastest possible path from intent to action.
A common concern with Passkeys is what happens when a user switches from an iPhone to a Windows machine. Modern passkeys are "synced" credentials. If a user saves a passkey to their iCloud Keychain or Google Password Manager, it's available across all their devices within that ecosystem.
However, you should always implement a fallback for users on older browsers or those who lose access to their primary authenticator. Zyphr allows you to link multiple credentials to a single Subscriber ID. You might offer:
Managing these fallbacks is often where costs and complexity explode. Many providers charge per "active user" or per "MFA attempt." With Zyphr, we provide a unified identity model. Whether a user logs in via Passkey, Google, or Email, they are the same Subscriber ID. Your messaging logic remains identical regardless of how they proved who they are.
While phishing resistance is the headline feature, passkeys also protect you from credential stuffing. Since there is no password to leak, an attacker who gains a list of emails from another site's breach cannot test those credentials against your app.
Furthermore, passkeys prevent "Man-in-the-Middle" (MITM) attacks. During the WebAuthn ceremony, the browser signs the origin of the request. If a user is on fake-app.com, the signature will be tied to that domain. When your server (expecting a signature for real-app.com) receives it, the verification will fail. This happens at the protocol level, meaning even the most sophisticated phishing kits cannot bypass it.
Building a secure, fast login experience shouldn't require a dedicated identity team. By moving to Passkeys, you're removing the "Forgot Password" barrier that prevents users from returning to your application.
If you're still using separate vendors for auth and notifications, you're paying a hidden tax in both time and money. Every hour spent debugging a failed webhook between your identity provider and your email gateway is an hour you aren't spending on your core product features.
To move forward:
zyphr.auth.passkeys.register().Stop gluing disparate services together. Start building with a platform that understands identity and communication are two sides of the same coin. Your users—and your on-call rotation—will thank you for the lack of friction.