← All writing

Should you build sign-in without passwords?

Emailed codes and social sign-in remove the password database and the reset flow. What you gain, what you take on, and the detail everyone gets wrong.

Should you build sign-in without passwords?

For most new products, yes. Dropping passwords removes four things you would otherwise own forever: a password database worth stealing, a reset flow, the rules about length and reuse, and the support requests that come from all three. You replace them with two dependencies, email delivery and the identity providers you accept, and one detail that almost everyone gets wrong the first time.

Here is the trade in full, including the part that bites.

What "passwordless" actually means

Two mechanisms, usually together:

A code by email. Someone types their address, you email a short code, they type it back. There is nothing to remember and nothing stored that is worth stealing.

Social sign-in. Continue with Google or Apple. The provider proves who they are, and you never handle a credential at all.

Offering both covers nearly everyone: people who have a Google or Apple account use the button, and everyone else uses their email.

What you gain

No password database. The single most valuable property. A stolen password file is a breach that follows your customers to every other site where they reused that password. If you never had one, that entire class of incident cannot happen to you.

No reset flow. Password reset is a small feature with a large surface: token generation, expiry, single use, and a set of enumeration questions about whether "no account with that address" is a safe thing to say. Passwordless sign-in and password reset are nearly the same flow, so you build it once instead of twice.

Less support. "I cannot log in" is one of the highest-volume support categories for any product, and most of it is password-shaped.

Better completion. Fewer fields and nothing to remember, on a phone especially.

What you take on

Be honest about these, because they are real.

Your sign-in now depends on email delivery. If your mail lands in spam, nobody can get in. This turns deliverability from a marketing concern into an availability concern, and it is the strongest argument for setting up your DNS records properly before launch rather than after.

Sign-in gets slower. Switching to an inbox and back is more work than a saved password in a browser. Social sign-in avoids this for the people who use it, which is a good reason to offer both.

You depend on identity providers. If someone loses access to their Google account, they lose access to yours. Accepting email codes as well as social sign-in gives them a route back.

Codes are a credential. They need the same care as a password: hashed at rest, short expiry, a limited number of attempts, and rate limits per address and per network. A six-digit code with unlimited guesses is a four-digit problem.

The detail everyone gets wrong

Here is the one, and it is worth reading twice.

Somebody signs up with [email protected] using an emailed code. Two months later they come back and click "Continue with Google", which is the same address.

Do they get their existing account, or a brand new empty one?

Get this wrong and you create a silent duplicate. The customer sees an empty account, assumes their data is gone, and writes to support saying they lost everything. Meanwhile you now have two account records for one human, possibly two subscriptions, and a reconciliation job to do by hand.

The fix is structural, not clever: every route into the system creates or finds an account through exactly one function. Email codes call it. Google calls it. Apple calls it. Anything you add later calls it. That function looks up by email address and decides once.

The reason to do it this way rather than "carefully, in three places" is that three places is how it breaks. Each provider gets added at a different time by a different person, and the third one forgets.

One chokepoint. Every time.

The verification wrinkle

There is a subtlety underneath that, and it matters for security rather than convenience.

Linking accounts by email address is only safe if you trust that the provider verified the address. Google and Apple do. An identity provider that lets someone claim an arbitrary unverified email would let an attacker take over an existing account by signing in with its address.

So: link on email, but only when the address is verified, and treat "verified" as something you check rather than assume.

What good looks like

A checklist you can hold your implementation against:

  • Codes are stored hashed, never in plain text, and never written to a log.
  • Codes expire quickly, in minutes, not hours.
  • A limited number of attempts, then the code is dead.
  • Rate limits per email address and per network address, so nobody can request fifty codes for someone else's inbox.
  • One account-creation chokepoint shared by every sign-in route.
  • Email linking only on verified addresses.
  • Sessions invalidated server-side on sign out, not just the cookie dropped.
  • Both email and social offered, so neither is a single point of failure.

When passwords still make sense

Two cases, mainly.

Your customers cannot receive email reliably. Shared inboxes, locked-down corporate mail, or a context where checking email mid-task is genuinely disruptive.

Enterprise requirements. Larger customers may want single sign-on through their own identity provider, which is a different project. The good news is that a codebase built around one account-creation chokepoint is the easy version of adding it later.

The summary

Passwordless sign-in trades a password database and a reset flow for a dependency on email delivery. For most products that is a clearly good trade, provided you do two things: take deliverability seriously, because sign-in now depends on it, and route every sign-in method through one account-creation function so that the same person always gets the same account.

This is one of the four things worth not building yourself, precisely because the account-merge bug looks like working code right up until it does not.

Keep reading