A webhook on user.created and user.updated that replaces a bad username.
Runs in a webhook route. Every name is checked with context: "username" and the teen policy; change the policy to suit the audience. Replace the key.
// app/api/webhooks/clerk/route.ts: refuse a username at user.created / user.updated
import { Unprofane } from '@unprofane/sdk';
const unprofane = new Unprofane(process.env.UNPROFANE_API_KEY!);
/** Returns the reason a username cannot be shown, or null when it can. */
export async function usernameProblem(username: string): Promise<string | null> {
const r = await unprofane.check({ text: username, context: 'username', policy: 'teen' });
if (r.allowed) return null;
return r.matches[0]?.category === 'reserved'
? 'That name is reserved.'
: 'Pick a different username.';
}
export async function POST(req: Request) {
const event = await req.json();
if (event.type === 'user.created' || event.type === 'user.updated') {
const problem = await usernameProblem(event.data.username ?? '');
if (problem) await clerkClient.users.updateUser(event.data.id, { username: `user_${event.data.id.slice(-8)}` });
}
return new Response('ok');
}Clerk lets the username through first, so the hook renames it a moment later; show the person a prompt to choose again. Verify the webhook signature with Clerkâs helper before this code runs.
What the response holds, and how to read a reserved name apart from a profane one, is in the API reference. The other recipes are on the username page.