#mutation after user authentication
19 messages · Page 1 of 1 (latest)
Hi @past maple. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
You have a few options, depending on which auth setup you’re using.
1. If you’re using Convex Auth (convexAuth)
Convex Auth exposes callbacks that run inside the same transaction when a user is created or updated. This is the cleanest place to:
- initialize related tables (many‑to‑many, etc.)
- set custom user properties like
roleandnickname
Use the afterUserCreatedOrUpdated callback:
import GitHub from "@auth/core/providers/github";
import Password from "@convex-dev/auth/providers/Password";
import { MutationCtx } from "./_generated/server";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub, Password],
callbacks: {
async afterUserCreatedOrUpdated(ctx: MutationCtx, { userId }) {
// 1. Set custom user properties
await ctx.db.patch(userId, {
role: "member",
nickname: "New user",
});
// 2. Initialize related tables (example many-to-many setup)
await ctx.db.insert("userCourses", {
userId,
courseId: "someDefaultCourseId",
});
// more inserts/patches as needed...
},
},
});
This runs automatically whenever a user is created or updated, and you can do arbitrary writes via ctx.db. [Writing data during auth]
If you need to pass extra data (like which cohort/course to enroll in) from the sign‑up flow into this callback, the Discord thread points to using the provider’s customization hooks (e.g. Password provider’s “customize user information”) to carry that data through, but the exact code for that is not in the provided sources. [Discord: mutation after sign-in]
2. If you’re using an external auth kit (WorkOS AuthKit, Better Auth, Clerk, etc.)
These integrations expose event hooks or triggers you can use similarly.
WorkOS AuthKit
You can listen to user.created and user.updated and then:
- create your own
usersrow with extra fields - initialize related tables
// convex/auth.ts
import { AuthKit, type AuthFunctions } from "@convex-dev/workos-authkit";
import { components, internal } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
const authFunctions: AuthFunctions = internal.auth;
const authKit = new AuthKit<DataModel>(components.workOSAuthKit, {
authFunctions,
});
export const { authKitEvent } = authKit.events({
"user.created": async (ctx, event) => {
const userId = await ctx.db.insert("users", {
authId: event.data.id,
email: event.data.email,
name: `${event.data.firstName} ${event.data.lastName}`,
role: "member",
nickname: event.data.firstName ?? "New user",
});
// Initialize related tables
await ctx.db.insert("userCourses", {
userId,
courseId: "someDefaultCourseId",
});
},
"user.updated": async (ctx, event) => {
const user = await ctx.db
.query("users")
.withIndex("authId", (q) => q.eq("authId", event.data.id))
.unique();
if (!user) {
console.warn(`User not found: ${event.data.id}`);
return;
}
await ctx.db.patch(user._id, {
email: event.data.email,
name: `${event.data.firstName} ${event.data.lastName}`,
});
},
});
Better Auth
Better Auth has “triggers” that run in the same transaction when its auth tables change. You can use them to initialize your own tables:
// convex/auth.ts
import { DataModel } from "./_generated/dataModel";
import { components, internal } from "./_generated/api";
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
const authFunctions: AuthFunctions = internal.auth;
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions,
triggers: {
user: {
onCreate: async (ctx, doc) => {
// doc is the Better Auth user document
await ctx.db.insert("users", {
authId: doc._id,
role: "member",
nickname: "New user",
});
await ctx.db.insert("userCourses", {
userId: doc._id,
courseId: "someDefaultCourseId",
});
},
},
},
});
export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi();
Clerk (webhooks)
For Clerk, the docs show using webhooks to upsert/delete users in your own users table. You can extend those mutations to set role, nickname, and initialize related tables:
// convex/users.ts
import { internalMutation, QueryCtx } from "./_generated/server";
import { UserJSON } from "@clerk/backend";
import { v, Validator } from "convex/values";
export const upsertFromClerk = internalMutation({
args: { data: v.any() as Validator<UserJSON> },
async handler(ctx, { data }) {
const userAttributes = {
name: `${data.first_name} ${data.last_name}`,
externalId: data.id,
role: "member",
nickname: data.first_name ?? "New user",
};
const user = await userByExternalId(ctx, data.id);
if (user === null) {
const userId = await ctx.db.insert("users", userAttributes);
// Initialize related tables
await ctx.db.insert("userCourses", {
userId,
courseId: "someDefaultCourseId",
});
} else {
await ctx.db.patch(user._id, userAttributes);
}
},
});
[[Clerk webhook upsert](<https://docs.convex.dev/auth/database-auth#mutations-for-upserting-and-deleting-users>)]
---
### 3. Why not “just run a mutation after sign‑in” from the client?
Several Discord threads show that calling a mutation immediately after `signIn(...)` on the client can hit a race condition where the auth state isn’t yet available to Convex, so `getUserIdentity` / `getAuthUserId` returns `null`. [[Discord: mutation after sign-in](<https://discord.com/channels/1019350475847499849/1401184973746081843>)]
That’s why the recommended patterns are:
- server‑side callbacks (`afterUserCreatedOrUpdated` with Convex Auth)
- auth‑component events/triggers (WorkOS AuthKit, Better Auth)
- identity‑provider webhooks (Clerk, Auth0, etc.)
These run reliably at the moment the user is created/updated and can safely do your initialization work.
---
If you share which auth integration you’re using (Convex Auth, Clerk, Auth.js, WorkOS, Better Auth, etc.), I can narrow this down to a concrete snippet tailored to your setup.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
@past maple Kapa.ai is still learning and improving, please let me know how I did by reacting below.
acutally the first point was my solution, I'm wondering is there any way to omit the "OrUpdated" part? I only need to run this once the user is created and not when he is updated.
@heady helm acutally the first point was my solution, I'm wondering is there any way to omit the "OrUpdated" part? I only need to run this once the user is created and not when he is updated.
Let me check the knowledge base for you again. One moment! :books:
There’s no way (in the docs) to make afterUserCreatedOrUpdated fire only on creation. It is explicitly defined to run “after a user is created or updated”. [Writing additional data; afterUserCreatedOrUpdated ref]
To run logic only once, you’ll need to add your own guard inside the callback, for example by:
- checking some field on the user document (e.g.
initialized: boolean) and only running when it’s missing/false, or - checking whether related records already exist in your own tables and skipping if they do.
The knowledge sources don’t show a dedicated “onCreate-only” Convex Auth callback.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@past maple Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@heady helm how to use zod to validate user email upon account creation request (using convex auth)
Let me check the knowledge base for you again. One moment! :books:
You can plug Zod into Convex Auth’s password provider to validate the email when the account is created.
Use the profile option on the Password provider and validate params with a Zod schema; throw a ConvexError if it’s invalid. [Customize sign-up validation]
Example:
// convex/auth.ts
import { ConvexError } from "convex/values";
import { Password } from "@convex-dev/auth/providers/Password";
import { z } from "zod";
const ParamsSchema = z.object({
email: z.string().email(), // Zod email validation
});
export default Password({
profile(params) {
const { error, data } = ParamsSchema.safeParse(params);
if (error) {
// This aborts account creation and returns a structured error
throw new ConvexError(error.format());
}
// Return the validated profile fields that should be stored
return { email: data.email };
},
});
This runs on sign‑up (and any time the profile is set via this provider). If the email is invalid, the ConvexError prevents the account from being created and you can surface the error on the client. [Customize sign-up validation]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@past maple Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@heady helm // convex/auth.ts
import { ConvexError } from "convex/values";
import { Password } from "@convex-dev/auth/providers/Password";
import { z } from "zod";
const ParamsSchema = z.object({
email: z.string().email(), // Zod email validation
});
export default Password({
profile(params) {
const { error, data } = ParamsSchema.safeParse(params);
if (error) {
// This aborts account creation and returns a structured error
throw new ConvexError(error.format());
}
// Return the validated profile fields that should be stored
return { email: data.email };
},
}); does this overwrite other actions? I mean does this completely remove the password saving logic etc?
Let me check the knowledge base for you again. One moment! :books:
No, using Password({ profile(...) { ... } }) does not replace the rest of the password provider logic.
From the docs, profile is just one optional field on PasswordConfig:
Perform checks on provided params and customize the user information stored after sign up, including email normalization. Called for every flow ("signUp", "signIn", "reset", "reset-verification" and "email-verification"). [Password config]
The password hashing, storage, and verification are handled by the Password provider itself:
Email and password authentication provider. Passwords are by default hashed using Scrypt from Lucia. You can customize the hashing via the
cryptooption. [Password provider]