// ai summary · how i work · part 2·27 Oct 2026·7 min read

Using BDD to add Passkey authentication to rackd2

Part 1 showed how Gherkin stories act as the contract for Rackd2's MCP tools. This part tests the claim on a real feature, start to finish: I ask for a feature, approve the .feature file, and never look at the code. The loop only closes because the verification is strong enough that I don't have to.

AI summary —Written by an AI agent from my brief and the Rackd2 repo. The spec is copied byte-for-byte from the repo, and the test numbers come from a real run. I didn't write the prose by hand.
GherkinPasskeys · WebAuthnSimpleWebAuthnBDD with AI agents · part 2

1. The prompt

My whole prompt, tidied up:

Add passkey login. Default to email or passkey; keep username and password, but discourage it.

No ticket, no design doc.

2. The agent drafts the spec

Before writing any code, the agent read the existing auth code. Rackd2 already had a magic-link flow, so email login could reuse it. Next it chose a library: SimpleWebAuthn instead of a full auth framework. Then it wrote the behaviour as Gherkin, in my words rather than in code.

It came back with 15 scenarios and two open questions:

  1. Should email login stay a link or become a 6-digit code?
  2. How long should "Not now" on the passkey offer be remembered? (proposed: 30 days)

3. I approve

My entire review:

yeah great do it

I accepted both defaults: the magic link stays, and "Not now" is remembered for 30 days. I reviewed the spec and nothing else. This is the approved file, verbatim from features/web/login.feature:

gherkin
Feature: Logging in without a password
  Email links and passkeys are the easy way in. A password still works, but it is tucked away.

  Scenario: The login page leads with email and passkey
    When I open the login page
    Then I see a "Continue with email" button
    And I see a "Sign in with a passkey" button
    And I do not see a password field
    And I see a small "Use a password instead" link

  Scenario: Logging in with an email link
    Given I have an account with email "lifter@example.com"
    When I ask for a login link for "lifter@example.com"
    Then I am told to check my inbox
    And a login link is emailed to "lifter@example.com"
    When I open the login link
    Then I am logged in

  Scenario: An unknown email gets the same answer, but no email
    When I ask for a login link for "nobody@example.com"
    Then I am told to check my inbox
    And no email is sent

  Scenario: A login link works only once
    Given I have an account with email "lifter@example.com"
    And I have used my login link
    When I open the same login link again
    Then I am not logged in
    And I am told "This login link is invalid or has expired. Request a new one."

  Scenario: An expired login link is refused
    Given I have an account with email "lifter@example.com"
    And my login link is 16 minutes old
    When I open the login link
    Then I am not logged in
    And I am told "This login link is invalid or has expired. Request a new one."

  Scenario: Too many login links in an hour
    Given I have an account with email "lifter@example.com"
    And I asked for 5 login links in the last hour
    When I ask for a login link for "lifter@example.com"
    Then I am told to check my inbox
    And no email is sent

  Scenario: I am offered a passkey after logging in by email
    Given I have an account with no passkey
    When I log in with an email link
    Then I am offered "Add a passkey for faster login"
    And I can choose "Not now"

  Scenario: Adding a passkey
    Given I am logged in
    When I add a passkey named "iPhone"
    Then my settings list a passkey named "iPhone"
    And I am not offered a passkey again

  Scenario: Saying "Not now" is remembered for a while
    Given I have an account with no passkey
    And I chose "Not now" yesterday
    When I log in with an email link
    Then I am not offered a passkey

  Scenario: Logging in with a passkey
    Given I have an account with a passkey
    When I sign in with my passkey
    Then I am logged in

  Scenario: Cancelling the passkey prompt
    When I start signing in with a passkey
    And I cancel the passkey prompt
    Then I am not logged in
    And I am told "Passkey sign-in was cancelled. Try again or continue with email."

  Scenario: A passkey this site does not know is refused
    Given a passkey that belongs to no account
    When I sign in with that passkey
    Then I am not logged in
    And I am told "We don't recognise this passkey. Continue with email instead."

  Scenario: Removing a passkey
    Given I am logged in with a passkey named "iPhone"
    When I remove the passkey "iPhone"
    Then my settings list no passkeys
    And signing in with that passkey is refused

  Scenario: Logging in with a password still works
    Given I have an account with email "lifter@example.com" and password "squat-heavy-1"
    When I open the login page
    And I choose "Use a password instead"
    And I log in with "lifter@example.com" and "squat-heavy-1"
    Then I am logged in

  Scenario: A wrong password is refused
    Given I have an account with email "lifter@example.com" and password "squat-heavy-1"
    When I log in with "lifter@example.com" and "wrong-password"
    Then I am not logged in
    And I am told "Wrong email or password."

4. The agent implements

I didn't read any of this. Here's a summary for anyone who wants to know what's under the hood:

  • SimpleWebAuthn v14 (@simplewebauthn/server + @simplewebauthn/browser) handles both registration and authentication.
  • Discoverable credentials (resident keys): "Sign in with a passkey" works without typing an email first. The authenticator says who you are.
  • Single-use challenges that expire after 5 minutes. A challenge is deleted as soon as it's used, so it can't be replayed.
  • Migration 0009 runs in Rackd2's SQLite-backed Durable Object. It adds a passkeys table, a webauthn_challenges table and a passkey_prompt_dismissed_at column on users, which stores the "Not now" answer.
  • Login page UX. "Continue with email" and "Sign in with a passkey" are the two big buttons. No password field is shown. "Use a password instead" is a small link.
  • Passkey offer after email login. "Add a passkey for faster login" with a "Not now" option, remembered for 30 days.
  • Settings. A Passkeys list shows each passkey's name and when it was added and last used. "Add a passkey" asks you to name the new one, and each passkey has a Remove button. A removed passkey is refused at sign-in.

5. Verification: why I don't need to read the code

This is the part that closes the loop. Each check is something I can trust without opening a diff:

  1. The spec wasn't touched. The file in the repo was compared with the approved draft using cmp, and they're byte-identical. The agent can't "fix" a failing test by quietly changing the story.
  2. Everything is green. 489 BDD tests and 31 unit tests pass, and tsc is clean.
  3. Real WebAuthn, not a stub. The steps use a software WebAuthn authenticator that creates real attestation and assertion responses. SimpleWebAuthn verifies them exactly as it would in production, so the crypto path is actually exercised.
  4. The tests can fail. As a mutation check, the agent broke the implementation on purpose twice, and both times the scenarios you'd expect (and only those) went red. A suite that can't fail proves nothing. This one fails where it should.
  5. Live smoke check. After deploying, the live login page showed the new layout and the email and passkey endpoints responded as expected.
  6. Shipped in a single commit: 154e66a.

The last manual check

The software authenticator proves the server logic and the protocol, not how each browser and OS behaves. So I did the one manual step: I logged in by email, accepted "Add a passkey", and signed in with my passkey on a real device. It just worked. That was the first time I touched the feature, and it was as a user, not as a reviewer. If another device ever reveals an edge case, it becomes a new scenario first.

Takeaway

The time I spent on this feature was one prompt, a read of 15 plain-English scenarios, a five-word approval, and one real passkey sign-in. The spec is the interface between me and the agent. The checks (an untouched spec, a green suite, real crypto and a mutation check) are what let me skip the code review.

Try it at rackd2.davidbroza.dev · tool reference at /docs/mcp