Previously every account without a custom avatar automatically got
its email MD5-hashed and sent to gravatar.com at signup, with no way
to turn it off. Adds users.useGravatar (default false): removed the
automatic signup-time lookup entirely, and "remove photo" now falls
back to the initials placeholder instead of silently re-deriving a
Gravatar URL. New toggle in Settings -> Profile, off by default,
description explains the MD5-hash-to-third-party tradeoff. Existing
accounts' current avatarUrl is left untouched either way — no
retroactive avatar changes for anyone already using one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Usernames are auto-assigned at signup (shipped earlier this session)
but there was still no way to change one. Adds a Username field to
Settings, validated client- and server-side against the same
3-20-char lowercase/digits/underscore pattern used for generation,
with a 409 on collision (excluding the user's own current username).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New users get a Gravatar-backed avatar automatically (computed from email
at signup); users can upload a custom photo instead via Settings, or
revert to the Gravatar/initials fallback. avatarUrl stays the single
resolved value (custom photo, OAuth photo, or precomputed Gravatar URL)
so every existing avatar-rendering spot across the app needs zero changes.
- users.hasCustomAvatar tracks whether avatarUrl is a real upload vs a
computed Gravatar fallback, so "remove photo" knows what to revert to.
- New /api/v1/upload/avatar-presign route (session-scoped, generic —
the existing recipe-photo presign route required a recipeId).
- CSP img-src needed www.gravatar.com added, or every browser blocks
the fallback avatar outright.
- Settings page now reads avatarUrl from a fresh DB query instead of
Better Auth's session object — the session cookie cache (5 min TTL)
was serving a stale image right after upload, showing the initials
fallback until the cache happened to expire.
Verified locally: signup auto-sets a Gravatar URL, upload persists
across reload, remove correctly reverts to Gravatar/initials.
- Private accounts: users.isPrivate hides a user from search and their
recipes from search/trending/for-you discovery surfaces (follow-aware
where the route already has session context, blanket exclusion where it
doesn't); existing followers and direct links are unaffected, no
follow-request approval flow was built (explicit scope limit)
- Merged /people into the Explore tab (tab=people query param); the old
standalone route now redirects there
- "Get Ideas" vs "Surprise Me" were doing the same empty-prompt call;
Surprise Me now injects a real random constraint (5-ingredient, one-pot,
etc.), matching the existing pattern in the AI recipe-generate dialog
- Meal-plan day cells now link to their recipe (was dead text) and gained
a one-click "mark as cooked" action
- Theme toggle is now a real three-way light/dark/system control instead
of a binary flip
- Nutrition goals form had zero i18n wiring; fully localized now
New migration 0027 (users.is_private) generated, left unapplied like the
others. Verified with typecheck, lint, and a clean --no-cache docker build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow/unfollow users. Recipe favorites. Threaded comments with emoji reactions.
Collections (public/private) with shared member invite. Activity feed.
Public profile pages at /u/[username].