Danny I
Danny Iā€¢4mo ago

Sign up user with custom fields

(Using convex auth/Password) Hey, so following the docs, I need custom fields for my users (who doesn't?), so I make a custom Password provider as specified for the password sign in/up function. But now when I need to set up email verification, the custom Password provider is erroring, since it's not callable. Code:
import Password from "./CustomPassword";

export const { auth, signIn, signOut, store } = convexAuth({
providers: [Password({ verify: ResendOTP })] // <-- This is not callable
});
import Password from "./CustomPassword";

export const { auth, signIn, signOut, store } = convexAuth({
providers: [Password({ verify: ResendOTP })] // <-- This is not callable
});
https://labs.convex.dev/auth/config/passwords#email-verification-setup And the custom provider šŸ¤·šŸ»ā€ā™‚ļø Does it only run on sign up? Is it okay to initialize values for new accounts here?
import { Password } from "@convex-dev/auth/providers/Password";
import { DataModel } from "./_generated/dataModel";
import { z } from "zod";
import { ConvexError } from "convex/values";

const ParamsSchema = z.object({
email: z.string().email(),
password: z.string().min(16),
});


export default Password<DataModel>({
profile(params) {
const { error, data } = ParamsSchema.safeParse(params);
if (error) {
throw new ConvexError(error.format());
}
return {
email: params.email as string,
name: params.name as string,
accountType: params.accountType as 'personal' | 'business',
businesses: params.accountType === 'business' ? [] : null,
businessName: params.accountType === 'business' ? params.businessName as string : null,
conversationIds: [],
};
},
});
import { Password } from "@convex-dev/auth/providers/Password";
import { DataModel } from "./_generated/dataModel";
import { z } from "zod";
import { ConvexError } from "convex/values";

const ParamsSchema = z.object({
email: z.string().email(),
password: z.string().min(16),
});


export default Password<DataModel>({
profile(params) {
const { error, data } = ParamsSchema.safeParse(params);
if (error) {
throw new ConvexError(error.format());
}
return {
email: params.email as string,
name: params.name as string,
accountType: params.accountType as 'personal' | 'business',
businesses: params.accountType === 'business' ? [] : null,
businessName: params.accountType === 'business' ? params.businessName as string : null,
conversationIds: [],
};
},
});
27 Replies
Convex Bot
Convex Botā€¢4mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
sshader
sshaderā€¢4mo ago
Re: not callable error -- you either want to pass verify: ResendOTP in your CustomPassword.ts file, or make CustomPassword.ts export a function that accepts an argument like { verify: EmailConfig } and passes that to the @convex-dev/auth Password Re: when is profile called -- https://labs.convex.dev/auth/api_reference/providers/Password#profile states "Called for every flow ("signUp", "signIn", "reset", "reset-verification" and "email-verification")." Also take a look at https://labs.convex.dev/auth/setup/schema#customizing-the-users-table -- the fields returned by profile should be automatically added to the entries in your users table
providers/Password - Convex Auth
Authentication library for your Convex backend
Customizing Schema - Convex Auth
Authentication library for your Convex backend
Danny I
Danny IOPā€¢4mo ago
So it seems it isn't safe to do the initialization data of new accounts in profile, I guess I should do it in the front end when I call sign up. And, thanks, I will try modifying the custo mpassword provider to accept an optional params object, I'll try and copy the default one.
sshader
sshaderā€¢4mo ago
So it seems it isn't safe to do the initialization data of new accounts in profile,
Depends what you mean by safe -- it'll only modify the authenticated user (so not unsafe from a security perspective), but it'll run on things like sign in too, so unless you add a custom createOrUpdateUser callback, you could end up overwriting fields on existing users
Danny I
Danny IOPā€¢4mo ago
I might have got it working (which then I can see if I'm clobbering my users), but, sending the verification email is failing:
async sendVerificationRequest({ identifier: email, provider, token }) {
const resend = new ResendAPI(provider.apiKey);
const { error } = await resend.emails.send({
from: "ChatPlanAI <onboarding@resend.dev>",
to: [email],
subject: ` ChatPlanAI: Activate`,
text: "Your code is " + token
});

if (error) {
throw new Error("Could not send");
}
},
async sendVerificationRequest({ identifier: email, provider, token }) {
const resend = new ResendAPI(provider.apiKey);
const { error } = await resend.emails.send({
from: "ChatPlanAI <onboarding@resend.dev>",
to: [email],
subject: ` ChatPlanAI: Activate`,
text: "Your code is " + token
});

if (error) {
throw new Error("Could not send");
}
},
[CONVEX A(auth:signIn)] [Request ID: a2d1c5af20cdd5ad] Server Error
Uncaught Error: Missing environment variable `SITE_URL`
at requireEnv (../../node_modules/@convex-dev/auth/dist/server/utils.js:4:4)
}
[CONVEX A(auth:signIn)] [Request ID: a2d1c5af20cdd5ad] Server Error
Uncaught Error: Missing environment variable `SITE_URL`
at requireEnv (../../node_modules/@convex-dev/auth/dist/server/utils.js:4:4)
}
What do I need to put for the SITE_URL? It's an expo app that serves to mobile and web, so it kind of has two? But I tried them and that didn't work.
sshader
sshaderā€¢4mo ago
I think you're hitting https://github.com/get-convex/convex-auth/issues/40 -- OTPs shouldn't really require a SITE_URL but they share code with magic links which leads to this error. I believe setting the SITE_URL to any value should unblock OTPs (e.g. npx convex env set SITE_URL http://localhost:3000)
GitHub
OTPs should not require SITE_URL Ā· Issue #40 Ā· get-convex/convex-au...
https://discord.com/channels/1019350475847499849/1263635045483155556 It's because we always generate the URL that includes the code param, even in the case of OTPs. This comes from the shape of...
Danny I
Danny IOPā€¢4mo ago
Ah nice! Yes, I set the SITE_URL locally, that makes sense why it didn't work. The next error is odd as well, it's showing an import for convex that doesn't exist, my project is in Documents, but it doesn't go Documents/convex, it's missing two directories in there... All the imports are relative inside of of the convex folder import { ResendOTP } from "./ResendOTP";, the generated import in the api folder looks correct too.
[CONVEX A(auth:signIn)] [Request ID: 7702befab96c587e] Server Error
Uncaught Error: Could not send
at sendVerificationRequest [as sendVerificationRequest] (../../convex/ResendOTP.ts:21:8)
at async handleEmailAndPhoneProvider (../../node_modules/@convex-dev/auth/dist/server/implementation/signIn.js:77:23)
at async signInViaProvider (../../node_modules/@convex-dev/auth/dist/server/implementation/index.js:373:8)
at async authorize [as authorize] (../../node_modules/@convex-dev/auth/dist/providers/Password.js:131:20)
at async handleCredentials (../../node_modules/@convex-dev/auth/dist/server/implementation/signIn.js:102:19)
at async handler (../../node_modules/@convex-dev/auth/dist/server/implementation/index.js:245:20)
Error: ENOENT: no such file or directory, open '/Users/daniel/Documents/convex/ResendOTP.ts' <----- This is strange
at Object.readFileSync (node:fs:453:20)
at getCodeFrame (/Users/daniel/Documents/Projects/chat-plan-ai/node_modules/metro/src/Server.js:949:18)
at Server._symbolicate (/Users/daniel/Documents/Projects/chat-plan-ai/node_modules/metro/src/Server.js:1026:22)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at Server._processRequest (/Users/daniel/Documents/Projects/chat-plan-ai/node_modules/metro/src/Server.js:419:7) {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/daniel/Documents/convex/ResendOTP.ts'
}
[CONVEX A(auth:signIn)] [Request ID: 7702befab96c587e] Server Error
Uncaught Error: Could not send
at sendVerificationRequest [as sendVerificationRequest] (../../convex/ResendOTP.ts:21:8)
at async handleEmailAndPhoneProvider (../../node_modules/@convex-dev/auth/dist/server/implementation/signIn.js:77:23)
at async signInViaProvider (../../node_modules/@convex-dev/auth/dist/server/implementation/index.js:373:8)
at async authorize [as authorize] (../../node_modules/@convex-dev/auth/dist/providers/Password.js:131:20)
at async handleCredentials (../../node_modules/@convex-dev/auth/dist/server/implementation/signIn.js:102:19)
at async handler (../../node_modules/@convex-dev/auth/dist/server/implementation/index.js:245:20)
Error: ENOENT: no such file or directory, open '/Users/daniel/Documents/convex/ResendOTP.ts' <----- This is strange
at Object.readFileSync (node:fs:453:20)
at getCodeFrame (/Users/daniel/Documents/Projects/chat-plan-ai/node_modules/metro/src/Server.js:949:18)
at Server._symbolicate (/Users/daniel/Documents/Projects/chat-plan-ai/node_modules/metro/src/Server.js:1026:22)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at Server._processRequest (/Users/daniel/Documents/Projects/chat-plan-ai/node_modules/metro/src/Server.js:419:7) {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/daniel/Documents/convex/ResendOTP.ts'
}
sshader
sshaderā€¢4mo ago
Not sure about the import (it looks like it might be a metro thing), but can you change the if (error) block in ResendOTP to console.error(error) in addition to throwing? I'll also make this change to the template
Danny I
Danny IOPā€¢4mo ago
Oh nice! That's a much better error
statusCode: 403,
message: 'You can only send testing emails to your own email address. To send emails to other recipients, please verify a domain at resend.com/domains, and change the `from` address to an email using this domain.',
name: 'validation_error'
statusCode: 403,
message: 'You can only send testing emails to your own email address. To send emails to other recipients, please verify a domain at resend.com/domains, and change the `from` address to an email using this domain.',
name: 'validation_error'
sshader
sshaderā€¢4mo ago
Ah our example app actually prints the error but the code snippet in docs doesn't. I'll fix that
Danny I
Danny IOPā€¢4mo ago
it wooooorkkssssss šŸ„³ thaaaank you So I'm still having issues signing up, I get this error:
Server Error: Failed to insert or update a document in table "users" because it does not match the schema: Object is missing the required field `accountType`.
Server Error: Failed to insert or update a document in table "users" because it does not match the schema: Object is missing the required field `accountType`.
Which is because in my callback for creating a user, args doesn't have the data I sent in for sign up, only the email is coming through:
callbacks: {
createOrUpdateUser: async (ctx, args) => {
const params = args.profile; // Only has email.
const userId = await ctx.db.insert("users", {
email: params.email as string,
name: params.name as string,
accountType: params.accountType as 'personal' | 'business',
businesses: params.accountType === 'business' ? [] : null,
businessName: params.accountType === 'business' ? params.businessName as string : null,
conversationIds: [],
});

return userId;
},
callbacks: {
createOrUpdateUser: async (ctx, args) => {
const params = args.profile; // Only has email.
const userId = await ctx.db.insert("users", {
email: params.email as string,
name: params.name as string,
accountType: params.accountType as 'personal' | 'business',
businesses: params.accountType === 'business' ? [] : null,
businessName: params.accountType === 'business' ? params.businessName as string : null,
conversationIds: [],
});

return userId;
},
Even though my profile function does receive the params and returns them:
export default Password<DataModel>({
profile(params) {
const { error, data } = ParamsSchema.safeParse(params);
if (error) { throw new ConvexError(error.format()); }

return {
email: data.email as string,
name: params.name as string,
accountType: params.accountType as 'personal' | 'business',
businesses: params.businesses as Id<"businesses">[] | null,
businessName: params.businessName as string | null,
conversationIds: params.conversationIds as Id<"conversations">[],
};
},
verify: ResendOTP
export default Password<DataModel>({
profile(params) {
const { error, data } = ParamsSchema.safeParse(params);
if (error) { throw new ConvexError(error.format()); }

return {
email: data.email as string,
name: params.name as string,
accountType: params.accountType as 'personal' | 'business',
businesses: params.businesses as Id<"businesses">[] | null,
businessName: params.businessName as string | null,
conversationIds: params.conversationIds as Id<"conversations">[],
};
},
verify: ResendOTP
šŸ‘€ These are the params I get in profile:
{
"accountType": "business",
"businessName": "Demo",
"conversationIds": [],
"email": d@gmail.com",
"flow": "signUp",
"name": "Danny",
"password": "12341234"
},
{
"accountType": "business",
"businessName": "Demo",
"conversationIds": [],
"email": d@gmail.com",
"flow": "signUp",
"name": "Danny",
"password": "12341234"
},
Also, the user is added to the database with the accountType field correctly set.
jamalsoueidan
jamalsoueidanā€¢4mo ago
show your user schema
Danny I
Danny IOPā€¢4mo ago
Edit: Actually it still didn't work. Hehe šŸ˜… Another silly moment I guess. I saw this
async createOrUpdateUser(ctx: MutationCtx, args) {
if (args.existingUserId) {
// Optionally merge updated fields into the existing user object here
return args.existingUserId;
}

// Implement your own account linking logic:
const existingUser = await findUserByEmail(ctx, args.profile.email);
if (existingUser) return existingUser._id;

// Implement your own user creation:
return ctx.db.insert("users", {
/* ... */
});
},
async createOrUpdateUser(ctx: MutationCtx, args) {
if (args.existingUserId) {
// Optionally merge updated fields into the existing user object here
return args.existingUserId;
}

// Implement your own account linking logic:
const existingUser = await findUserByEmail(ctx, args.profile.email);
if (existingUser) return existingUser._id;

// Implement your own user creation:
return ctx.db.insert("users", {
/* ... */
});
},
At which point I realized maybe I just need to return if the user was created successfully, which indeed fixed it.
users: defineTable({
email: v.string(),
name: v.string(),
accountType: v.union(v.literal("personal"), v.literal("business")),
businesses: v.union(v.array(v.id("businesses")), v.null()),
businessName: v.union(v.string(), v.null()),
conversationIds: v.array(v.id("conversations")),
}).index("email", ["email"]),
users: defineTable({
email: v.string(),
name: v.string(),
accountType: v.union(v.literal("personal"), v.literal("business")),
businesses: v.union(v.array(v.id("businesses")), v.null()),
businessName: v.union(v.string(), v.null()),
conversationIds: v.array(v.id("conversations")),
}).index("email", ["email"]),
I did find it confusing though since the logs didn't reflect that, and I expected this function to run on initial user creation šŸ¤·šŸ»ā€ā™‚ļø I'm sure there's a good reason for it though. I also may have spoken too soon, I still need to check that everything worked, it just didn't error this time. Ah, yeah, it didn't work, I'm not sure why it didn't error out that one time, but it is now, with the same error.
jamalsoueidan
jamalsoueidanā€¢4mo ago
click on the log where it says error take screenshot and past here
Danny I
Danny IOPā€¢4mo ago
No description
jamalsoueidan
jamalsoueidanā€¢4mo ago
ok delete your user from table
Danny I
Danny IOPā€¢4mo ago
all gone, it's all empty, including the auth sessions
jamalsoueidan
jamalsoueidanā€¢4mo ago
and console.log args right before the insert
Danny I
Danny IOPā€¢4mo ago
{
"argsInCreateCallback": {
"existingUserId": null,
"type": "credentials",
"provider": {
"id": "password",
"type": "credentials",
"options": {
"id": "password",
"crypto": {},
"extraProviders": [
null,
{
"id": "resend-otp",
"type": "email",
"name": "Resend",
"from": "Auth.js <no-reply@authjs.dev>",
"maxAge": 86400,
"options": {
"id": "resend-otp",
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
},
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
}
],
"verify": {
"id": "resend-otp",
"type": "email",
"name": "Resend",
"from": "Auth.js <no-reply@authjs.dev>",
"maxAge": 86400,
"options": {
"id": "resend-otp",
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
},
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
}
},
"crypto": {},
"extraProviders": [
null,
{
"id": "resend-otp",
"type": "email",
"name": "Resend",
"from": "Auth.js <no-reply@authjs.dev>",
"maxAge": 86400,
"options": {
"id": "resend-otp",
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
},
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
}
],
"verify": {...}
},
"profile": {
"accountType": "business",
"businessName": "Test Business",
"conversationIds": [],
"email": "danny.israel@gmail.com",
"name": "Danny"
},
"shouldLinkViaEmail": true,
"shouldLinkViaPhone": false
}
{
"argsInCreateCallback": {
"existingUserId": null,
"type": "credentials",
"provider": {
"id": "password",
"type": "credentials",
"options": {
"id": "password",
"crypto": {},
"extraProviders": [
null,
{
"id": "resend-otp",
"type": "email",
"name": "Resend",
"from": "Auth.js <no-reply@authjs.dev>",
"maxAge": 86400,
"options": {
"id": "resend-otp",
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
},
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
}
],
"verify": {
"id": "resend-otp",
"type": "email",
"name": "Resend",
"from": "Auth.js <no-reply@authjs.dev>",
"maxAge": 86400,
"options": {
"id": "resend-otp",
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
},
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
}
},
"crypto": {},
"extraProviders": [
null,
{
"id": "resend-otp",
"type": "email",
"name": "Resend",
"from": "Auth.js <no-reply@authjs.dev>",
"maxAge": 86400,
"options": {
"id": "resend-otp",
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
},
"apiKey": "re_8xzZwFbM_NcFZunViHTtiC3sEskBZJDgZ"
}
],
"verify": {...}
},
"profile": {
"accountType": "business",
"businessName": "Test Business",
"conversationIds": [],
"email": "danny.israel@gmail.com",
"name": "Danny"
},
"shouldLinkViaEmail": true,
"shouldLinkViaPhone": false
}
oh
jamalsoueidan
jamalsoueidanā€¢4mo ago
so you are using args.profile?
Danny I
Danny IOPā€¢4mo ago
I'm a dummy one second. let me try again
jamalsoueidan
jamalsoueidanā€¢4mo ago
well we always do same mistakes haha
Danny I
Danny IOPā€¢4mo ago
Okay it created account, I had altogether left out the initialization of the user, thinking it was somehow abstracted away since I didn't see it happenign earlier. But indeed it does actually run. Now the second callback is not running, which is a new issue but would love your quick outlook on it.
jamalsoueidan
jamalsoueidanā€¢4mo ago
what is the second callback?
Danny I
Danny IOPā€¢4mo ago
It might not be necessary anymore, but, using the userId I need to add a record in the businesses table, I think I can do this in the first callback though, on user creation since I do have the userId... šŸ¤”
afterUserCreatedOrUpdated: async (ctx, { userId, profile }) => {
const { businessName } = profile;
const businessId = await ctx.db.insert("businesses", {
ownerId: userId,
name: businessName as string,
projects: []
});
await ctx.db.patch(userId, { businesses: [businessId] });
},
afterUserCreatedOrUpdated: async (ctx, { userId, profile }) => {
const { businessName } = profile;
const businessId = await ctx.db.insert("businesses", {
ownerId: userId,
name: businessName as string,
projects: []
});
await ctx.db.patch(userId, { businesses: [businessId] });
},
My guess is I can just move this to the first callback and use the userId I get back when I insert the user record.
jamalsoueidan
jamalsoueidanā€¢4mo ago
just do it same time when you create the user šŸ™
Danny I
Danny IOPā€¢4mo ago
oh my god it's working. šŸ˜­ Thank you so much

Did you find this page helpful?