Stability isn't glamorous. Nobody tweets about "we fixed our rate limiting." But it's the kind of work that makes the difference between a platform you can trust and one that makes you nervous. This week we shipped a focused round of auth and stability improvements. Here's the full breakdown.
The password reset token fix
We had a subtle but real security issue in our password reset flow. When a user requested a password reset, we generated a random token, hashed it with SHA-256, and stored the hash in the database. So far, so good. The bug: we were also putting the hash in the reset URL instead of the raw token.
This completely defeats the purpose of hashing. If the URL contains the hash, and the database contains the hash, then anyone who intercepts the URL (from logs, email headers, browser history) already has exactly what they need to query the database directly. The raw token was generated but thrown away unused.
The fix is the classic pattern: raw token in the URL, hash in the DB. When a reset link is clicked, we hash the incoming URL token and compare it to what's stored. An attacker who sees the URL can't compute the hash preimage. An attacker who gets the DB can't compute the URL token. Both sides are now protected independently.
We also clean up any existing reset tokens for a user before issuing a new one — no more token pile-up in the database from forgotten requests.
Rate limiting on auth endpoints
None of our auth endpoints had rate limiting. Login, registration, and forgot-password were all wide open to automated abuse.
We added strict limits: 10 login attempts per 15 minutes per IP (after which you get a 429 with a Retry-After header), 5 registration attempts per hour per IP, and 3 password reset requests per hour per IP. These limits are tight enough to stop any realistic brute-force or spamming attempt, but generous enough that a real user who mistyped their password a few times won't notice.
All auth rate limits are keyed by IP, not by user, so they apply equally to both existing accounts and accounts that don't exist yet. This matters for the forgot-password endpoint specifically — we return the same success response whether or not an account exists (to prevent email enumeration), so the rate limit is the only real protection there.
AuthContext retry on transient failures
On page load, Tripplet fetches the current user from /api/auth/me to restore session state. Previously, if this request failed due to a transient server error — a cold start, a brief DB hiccup — the auth context would immediately conclude "not logged in" and show the signed-out UI.
We added a single retry with a short delay. If the first request returns a 5xx error or throws a network exception, we wait 800ms and try once more. For genuine session absences (no cookie, expired token) the first request returns a clean response, so there's no extra latency. The retry only fires when something actually went wrong server-side.
Error message hygiene
The registration endpoint was returning raw Supabase error messages on failure. These messages can include table names, constraint names, and other internal details that shouldn't be visible to users. We replaced that with a generic "Failed to create account. Please try again." that gives users something actionable without leaking implementation details.
We also added server-side email format validation to registration. Previously, an invalid email address would pass client-side validation, reach the database, and fail on a DB constraint — returning whatever error message Supabase gave us. Now we validate the format before touching the database.
What's next
There are more stability improvements in the pipeline. We're looking at adding structured error codes to all auth responses (so the client can distinguish "wrong password" from "account doesn't exist" from "service unavailable" without parsing error strings), implementing a proper refresh token strategy so sessions extend gracefully rather than expiring hard after 7 days, and adding a token revocation list for logout.
None of this is visible to users when it's working correctly. That's the point.