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](/blog/email-that-arrives) 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 `sam@example.com` 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](/blog/email-that-arrives) 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](/blog/what-your-agent-should-not-build), precisely because the account-merge bug looks like
working code right up until it does not.
