Every CRM starts the same way: email field, password field, confirm password field. It is the default because every authentication library ships with it. Supabase, our auth provider, makes password-based signup a single function call. It works. It is battle-tested. And we ripped it all out.
The decision was not ideological. We did not read a blog post about passwordless being the future and jump on the bandwagon. We made this call because our support inbox told us to. In the first three weeks after launch, 40% of all support tickets were authentication related. Password resets, locked accounts, users who signed up with Google OAuth and then tried to log in with a password they never set. The pattern was unmistakable: passwords were generating more support load than every other feature combined.
When your authentication system generates more support tickets than your entire product, the authentication system is the bug.
The concept is simple. User enters their email. We send a link. They click it. They are logged in. No password to remember, no password to forget, no password to reset. But the implementation details matter enormously for security and user experience.
When a user requests a magic link, our Supabase edge function generates a one-time token with a 10-minute expiration. The token is hashed with SHA-256 before storage, so even if our database were compromised, the tokens would be useless. The email contains a link to our auth callback endpoint with the raw token as a URL parameter. When the user clicks the link, we hash the incoming token and compare it against the stored hash. Match means login. Mismatch means the link was tampered with or already used.
Magic links have an inherent problem on mobile devices. When a user clicks the link in their email app, the operating system opens a browser. But which browser? On iOS, if the user's default browser is Chrome but they opened the email in the Mail app, the magic link opens in Safari. Now they are logged into a Safari session they will never use again, while Chrome, where they actually use SalesSheet, has no session.
We solved this with PKCE (Proof Key for Code Exchange). Before sending the magic link request, the client generates a random code verifier and computes a code challenge from it using SHA-256. The code challenge is sent with the magic link request. When the user clicks the link, they are redirected back to our app with an authorization code. The app then exchanges this code plus the original code verifier for a session. Because only the original client has the code verifier, only the original client can complete the login. It does not matter which browser opens the link; the session is established in the app that initiated the flow.
The implementation required a custom Supabase auth hook and modifications to our deep linking configuration on both iOS and Android. We use universal links on iOS and app links on Android to ensure the magic link always returns to our app rather than opening a random browser tab.
Killing passwords creates a dependency on email access. If a user loses access to their email, they lose access to their CRM. For a sales tool that contains deal pipelines, contact histories, and revenue data, this is not acceptable. We needed a recovery mechanism that does not reintroduce passwords through the back door.
When a user first logs in, we generate a 32-character recovery token and display it once. Just once. The UI makes it very clear: save this token somewhere safe, because we cannot show it again. We store a bcrypt hash of the token in the user's profile. If a user ever loses email access, they can enter their recovery token on our account recovery page. The token is verified against the stored hash, and if it matches, we allow the user to update their email address and trigger a new magic link to the new address.
This approach mirrors how cryptocurrency wallets handle seed phrases. The user holds the only copy of the plaintext. We hold only the hash. Neither party alone can compromise the account, but together they can recover it.
We considered both. SMS-based recovery introduces a phone number dependency and SIM-swapping risk. Authenticator apps like Google Authenticator or Authy add complexity that contradicts our entire reason for killing passwords in the first place. The recovery token is a single string that users can store in their password manager, write on a sticky note, or save in a secure note. It has zero ongoing maintenance.
Magic links create a new attack surface: email bombing. Without rate limiting, an attacker could enter a victim's email address and trigger thousands of magic link emails, filling their inbox and potentially triggering spam filters that would block legitimate emails. More subtly, each magic link request generates a database write (the hashed token), so an attacker could use unlimited requests as a denial-of-service vector against our infrastructure.
Our defense is layered:
We deployed passwordless authentication on a Tuesday. By Friday, the results were clear:
The best security mechanism is the one that users never have to think about. Magic links are invisible security. You click, you are in, and nobody else can be.
Killing passwords was one of the best decisions we made for SalesSheet, but it required more engineering work than simply enabling a Supabase feature flag. The PKCE flow for mobile, the recovery token system, and the multi-layered rate limiting each took serious thought and testing. If you are considering passwordless for your own product, here is what we would tell you:
Passwords are a solved problem in the sense that everyone knows how to implement them. They are an unsolved problem in the sense that users still cannot use them correctly. We chose to solve the problem by removing it entirely, and our users are better off for it.