Thursday, April 30, 2026
Day 28 — the last polish before something breaks
The week's loose ends — a real password reset flow, a forced first-login change, the marketing demo finalized, a stale migration that wouldn't apply cleanly. The kind of unglamorous work that means a real human can use the product without filing a ticket.
2 min read · Backfilled retrospective — written 2026-05-04
Daily entry
Day 28of building EquinePilot
Currently Day 77 · founder build log
Milestones reached
- ✓Password reset
- ✓Idempotent migrations
The last full work day before what turned out to be a much bigger Friday than I expected.
Password reset, properly. Earlier in the project I'd hand-waved this as "I'll send a magic-link email at some point." Today I built it for real:
- A
password_reset_tokenstable —userId,token(unique),expiresAt,usedAt,createdAt. /forgot-passwordroute, rate-limited at 1 token per 60 seconds. Always returns the same confirmation message regardless of whether the email exists in the system — that prevents enumeration leaks (an attacker would otherwise be able to probe for which emails are real users)./reset-password/[token]validates the token on page load (404 for invalid/expired/used) and re-validates inside the server action. Bcrypt-hashes the new password and marksusedAtin a single transaction.sendPasswordResetEmail()skips gracefully whenRESEND_API_KEYis absent — logs the URL to console for local dev so I don't have to bother with email infrastructure to test the flow.- "Forgot password?" link on the login page.
?reset=1query param triggers a success banner.
First-login forced password change. Trainers and clients get invited by managers, which means a temporary password gets generated and emailed. Before today, that temporary password was just... a regular password. They could keep using it forever. Now: middleware intercepts every non-API route when mustChangePassword=true on the user record and redirects to /change-password. The flag clears the moment they set their own. Applies to manager-invited clients, manager-invited trainers, and guardian provisional accounts.
Stale migration fix. 0013_testimonials had been silently failing because its journal when timestamp was backdated to May 2025 — Drizzle orders migrations by that timestamp when deciding what to apply, so it kept trying to apply this one before earlier migrations had run. Fixed the timestamp to 1ms after the prior migration's timestamp so the ordering is sane. Re-running pnpm db:migrate now works correctly.
Marketing site resilience. The homepage testimonials fetch was crashing the entire page when DATABASE_URL was absent — the DB module throws at evaluation time, not at first call. Moved it behind a dynamic import() + try/catch so the marketing site degrades to "no testimonials section" instead of "no marketing site."
Smaller pieces: server-action coverage on submitSignature (FormData parsing, policy enforcement, IP capture). Mobile pull-to-refresh on the dashboard and billing screens. A series of small typography fixes on the marketing demo from the day before.
What I didn't get to today, and didn't realize was about to consume tomorrow: the brand name itself. The placeholder I've been using all month is starting to feel placeholder-y, and the marketing site still doesn't have a real domain. I went to bed Wednesday night thinking Friday was going to be a quiet polish day. It was not.