fix: no user ever had a username, so people search found nobody

Root cause of "still cannot find users": username was optional in
better-auth's schema and no signup form or settings page ever set it,
but search/profile/follow all key on username, not user id. Every
account now gets a unique one auto-generated (from name/email) in the
user.create.before hook — covers email/password and OAuth signups.
Added a one-off db:backfill-usernames script for accounts created
before this fix and ran it against the current database.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-13 17:26:12 +02:00
parent 79cdb8cd04
commit 2ee99ea463
8 changed files with 112 additions and 5 deletions
+11 -2
View File
@@ -6,6 +6,7 @@ import { sendEmail, verifyEmailHtml, resetPasswordHtml, welcomeHtml } from "@/li
import { isSignupsDisabled } from "@/lib/site-settings";
import { findValidInvite, consumeInvite, INVITE_COOKIE } from "@/lib/invites";
import { gravatarUrl } from "@/lib/gravatar";
import { generateUniqueUsername } from "@/lib/username";
export const auth = betterAuth({
trustedOrigins: [process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"],
@@ -94,13 +95,21 @@ export const auth = betterAuth({
user: {
create: {
before: async (user, context) => {
if (!(await isSignupsDisabled())) return;
// No signup form or settings page ever lets someone set a username,
// yet profiles, follows, and people-search all key on it — so every
// account needs one generated here, or those features silently see
// nobody. OAuth signups may already have one (mapped from the
// provider profile); email/password never does.
const existingUsername = (user as { username?: string | null }).username;
const username = existingUsername || (await generateUniqueUsername(user.name || user.email));
if (!(await isSignupsDisabled())) return { data: { ...user, username } };
const token = context?.getCookie(INVITE_COOKIE);
const invite = token ? await findValidInvite(token, user.email) : null;
if (!invite) return false;
return { data: { ...user, role: invite.role, tier: invite.tier } };
return { data: { ...user, username, role: invite.role, tier: invite.tier } };
},
after: async (user, context) => {
// First registered user becomes admin
+8 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.13.0";
export const APP_VERSION = "0.13.1";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.13.1",
date: "2026-07-13 17:25",
fixed: [
"People search never found anyone, for anyone — there was no signup or settings flow that ever set a username, and search (along with profiles and follow) requires one. Every account now gets one automatically on signup; existing accounts were backfilled.",
],
},
{
version: "0.13.0",
date: "2026-07-13 15:37",
+33
View File
@@ -0,0 +1,33 @@
import { db, users, eq } from "@epicure/db";
/** Lowercase alnum-and-underscore slug, at least 3 chars, at most 20. */
function slugify(seed: string): string {
const base = seed
.split("@")[0]! // if seed is an email, drop the domain
.toLowerCase()
.replace(/[^a-z0-9_]/g, "")
.slice(0, 20);
return base.length >= 3 ? base : `${base}user`.slice(0, 20);
}
/**
* Every user needs a username — it's the only thing profile pages, follows,
* and people-search key on, but there's no signup-time or settings UI to set
* one. Called from the user.create.before hook (lib/auth/server.ts) so every
* account gets one automatically, generated from their name or email.
*/
export async function generateUniqueUsername(seed: string): Promise<string> {
const base = slugify(seed);
let candidate = base;
let suffix = 0;
while (true) {
const existing = await db.query.users.findFirst({
where: eq(users.username, candidate),
columns: { id: true },
});
if (!existing) return candidate;
suffix += 1;
candidate = `${base}${suffix}`.slice(0, 20);
}
}