FeedFilters
Pricing Log in Sign up

Blog

  • All posts
  • Subscribe (Atom)

Recent

  • Where the cookie boundary didn't May 12, 2026
  • A CDN for most of it May 11, 2026
  • Done, not abandoned May 8, 2026
  • There's no catch May 7, 2026
  • Sixteen mockups May 6, 2026
  • From scripts to infrastructure May 5, 2026
  • Doing mail myself May 4, 2026
  • Easier now than later May 2, 2026

Archive

  • 2026 15 posts

Passkeys, twice

April 21, 2026 · Kyle Cronin

I wanted FeedFilters to let people authenticate without trusting me with a password. Storing passwords is a category of responsibility I’d rather avoid — the breach risk, the ongoing decisions about hashing parameters and rate limits and lockouts. The usual escape hatch is to lean on a third party like Google or Apple for sign-in, but that brings its own baggage: a privacy story I don’t want, an external dependency I don’t want, and account-linking edge cases I don’t want. None of those felt like the right trade for a small indie service.

Passkeys are the modern alternative. The browser handles creation and storage on the user’s behalf, and modern devices sync them across the user’s other hardware via iCloud Keychain or 1Password or whatever they’re using. The user gets strong, phishing-resistant auth without having to remember anything; I get to not store passwords. That trade seemed right.

So passkeys are the default signup method on FeedFilters. There’s a password path too — an opt-in choice on a separate page — but the front door is a passkey. I made one design decision that I expect some people to push back on: when you add a passkey to your account, the default is for your password to be removed. The reasoning is that a passkey is a step up in security, and leaving a password sitting around as a secondary method undoes a lot of what the passkey gave you. If you’ve decided you want the stronger thing, I think it’s right to also retire the weaker thing, and that’s the path the UI recommends. The recovery story for losing a passkey is the same one you’d use for a forgotten password: a reset email. That’s the underlying reason FeedFilters verifies email addresses at signup — not for marketing reach, but so that the email channel is trustworthy as a recovery path.

That said, this started out as a hard rule rather than a default. Passkey-add unconditionally cleared the password, with a warning. I softened it later to a checkbox, on by default, that you can uncheck if you’d rather keep both factors. The cliff turned out to be harsher than necessary for users curious about passkeys but not yet ready to commit; the new default still nudges toward the stronger story, it just doesn’t force it.

The implementation didn’t go entirely smoothly.

The first thing that bit me was a WebAuthn detail I hadn’t expected to think about. The go-webauthn library validates a “backup eligibility” flag on every assertion: the stored credential’s BE flag has to match the one in the assertion. I’d shipped without persisting that flag, so it defaulted to zero, while real synced platform passkeys come back with BE set. Every login attempt from a synced passkey failed with “Backup Eligible flag inconsistency detected.” The fix was a small migration and a few lines of code — capture backup_eligible and backup_state at registration, refresh backup_state on each successful assertion — but finding it took some time with the spec. WebAuthn is a careful specification, which is right for what it’s protecting, but “careful” sometimes means “you have to handle subtleties you wouldn’t have thought of.”

The first UX iteration came the same evening. The day-two build had passkeys and passwords sitting side by side on the signup form — effectively equal options — and treating them that way undersold what passkeys are for. So I made passkey the default path: the signup page leads with a single passkey button, and there’s a quieter link below for the people who’d rather use a password, routed through a separate password signup page. The path of least resistance should be the better path, and I wanted that to be the case here.

The second iteration came a couple of weeks later, and was a fix for something I hadn’t realized I’d broken. The login page was firing a modal navigator.credentials.get on page load for any browser that supported PublicKeyCredential. The intent was friendly: if you have a passkey for FeedFilters, just pick it from the system prompt and you’re in. The reality was hostile: on every browser that didn’t have a passkey for the site, the WebAuthn modal would intercept whatever the user tried to do next, including their click into the password field. The modal would dismiss, the click would have been absorbed, and the user wouldn’t notice anything had gone wrong other than the login page seeming weirdly unresponsive.

The fix was to switch from auto-invoke to conditional UI, which is exactly what the API was designed for. With mediation: 'conditional' the browser doesn’t pop a modal at all; it surfaces available passkeys inline as autofill suggestions in the email field, alongside any saved emails. Users with a passkey see it offered in the natural place. Users without one are completely undisturbed. This is what the spec recommends, and I should have used it from the start.

Passkeys are the right default for a service like this, but the path to “passkeys are pleasant” is paved with details that aren’t obvious until you trip over them. Two iterations in, I think the login and signup experiences are finally where I want them. I’ll be curious to see what the third one needs.

About· Help· Blog· Privacy & Terms
FeedFilters by Flat Six Software · © 2026