kitdocs · github

Skill: add-password-reset

raw markdown skills/add-password-reset/SKILL.md

Built twice in two different apps. Almost none of the code carried over — different hashes, CSS, test harnesses. All of the decisions did. This skill is those decisions. The full step-by-step with code is in workflows/password-reset.md; email setup is workflows/email-sending.md.

Most of what makes a reset safe you already know: store a hash not the secret, answer the same way whether or not the account exists, rate-limit, end every session on success. Do all of that. This skill is the part that is not obvious, and the part that only bites in production.

Send a code. Never a link.

Default to a six-digit code typed back into the app. This is the opposite of the usual answer, so the reason matters:

Apps that people install to the home screen (PWAs) run in their own storage. On iOS a home-screen web app has its own WebKit storage container, separate from Safari's. A link tapped in Mail opens Safari, the new session cookie lands there, and the user returns to the app icon still logged out. Gmail's in-app browser splits the same way. A code is typed into the app that is asking for it, so it logs in there.

The argument for a link Why it loses here
"One tap is better UX" One tap into the wrong browser is a dead end.
"Codes get mistyped" A typo costs a retype. The split costs being locked out.
"It's the standard" The standard assumes a browser, not an installed app.
"Desktop users prefer it" Then add a link beside the code — never instead.
"Same origin, so the PWA is fine" Wrong — the most common mistake. iOS gives the installed app its own container for the same origin.
"The link will open the installed app" Android sometimes; iOS no. Don't design around it.

Red flag: if you catch yourself writing that a reset link is fine "because it is the same origin", "because the PWA is installed", or "because it opens the app" — stop and re-read this section.

Code-only is also less code: no token route, no expired-link page, no already-used state, no reset page that can 404.

Required properties (the checklist)

The email path

Check first whether the server can send mail at all (many VPS providers block ports 25/465/587). If not, use an HTTPS email API from an authenticated domain — see workflows/email-sending.md. Two things that cost an hour each:

Traps that only show in production

Verify before calling it done

  1. Unit-test the pure rules (code generation, expiry, attempt cap, hourly allowance) in whatever shape the repo tests.
  2. A browser flow at phone width, local dev only: the login link carries the typed address over, wrong code refused without losing the screen, right code logs in, old password dead, new one works, code refused the second time.
  3. Then once on production with a throwaway account on a +tag of your own inbox. Confirm: in Inbox not Spam, from noreply@<domain>, signed-by: <domain>. Delete the account afterwards.
  4. Confirm the live /auth/forgot returns no devCode.

What varies, so don't copy code

The hash (argon2 vs scrypt), the CSS system, the validation layer (zod vs JSON schema), whether the repo has a DB test harness, and where routes are registered. Read the target repo first; carry the decisions above, not files.