ianpaschal
ianpaschal2d ago

Convex Auth: How to add custom data to signIn() (or up)

Hello all, I'm trying to add additional fields to my sign up form which will be added to the users table. But I can't figure out how to pass them onward. Adding them to the FormData object passed to signIn() doesn't seem to work. How can one add these arbitrary fields (such as username)? I could also make it optional, but it shouldn't be, every account should have a username. So I'd prefer to set it via the sign up process. Any help much appreciated here. Also, would be happy to make a PR improving the documentation once I figure out how it's done. The documentation states:
You can only change fields to be required or add new required fields to the users table if all your authentication methods will provide those fields during sign-up (see how to do this for OAuth and Passwords).
But the linked page there doesn't actually explain how to pass that data to signIn() when the user submits the form. And evidently the FormData is not the correct method. Thanks for all help in advance!
20 Replies
Convex Bot
Convex Bot2d 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!
Gi
Gi2d ago
Update the user table (https://labs.convex.dev/auth/setup/schema) Add a profile callback on the provider and return the extra fields from the params (FormData)(https://labs.convex.dev/auth/config/passwords#customize-user-information)
Customizing Schema - Convex Auth
Authentication library for your Convex backend
Passwords - Convex Auth
Authentication library for your Convex backend
ianpaschal
ianpaschalOP2d ago
Yes, I'm aware of those two pages but they seem incomplete... Step 2 doesn't explain how to actually pass the values. I have added such a file, and added the username field and include it to the FormData, no dice. This is my form onSubmit, adapted slightly from the Convex Auth boilerplate:
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
formData.set("flow", flow);
formData.set("nameVisibility", 'hidden');
formData.set("locationVisibility", 'hidden');
void signIn("password", formData).catch((error) => {
setError(error.message);
});
}}
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
formData.set("flow", flow);
formData.set("nameVisibility", 'hidden');
formData.set("locationVisibility", 'hidden');
void signIn("password", formData).catch((error) => {
setError(error.message);
});
}}
I've also added this per the documentation:
import { Password } from "@convex-dev/auth/providers/Password";
import { DataModel } from "./_generated/dataModel";

export default Password<DataModel>({
profile(params) {
return {
email: params.email as string,
username: params.username as string,
locationVisibility: params.locationVisibility as 'hidden' | 'public',
nameVisibility: params.nameVisibility as 'hidden' | 'public',
};
},
});
import { Password } from "@convex-dev/auth/providers/Password";
import { DataModel } from "./_generated/dataModel";

export default Password<DataModel>({
profile(params) {
return {
email: params.email as string,
username: params.username as string,
locationVisibility: params.locationVisibility as 'hidden' | 'public',
nameVisibility: params.nameVisibility as 'hidden' | 'public',
};
},
});
However when signing up, I still get an error:
Uncaught Error: Failed to insert or update a document in table "users" because it does not match the schema: Object is missing the required field `locationVisibility`
Uncaught Error: Failed to insert or update a document in table "users" because it does not match the schema: Object is missing the required field `locationVisibility`
Gi
Gi2d ago
How does the schema look like?
ianpaschal
ianpaschalOP2d ago
export const visibilityLevels = v.union(
v.literal('hidden'),
v.literal('public'),
);

const userFields = {
email: v.string(),
avatarUrl: v.optional(v.string()),
givenName: v.optional(v.string()),
familyName: v.optional(v.string()),
countryCode: v.optional(v.string()),
username: v.string(),
nameVisibility: visibilityLevels,
locationVisibility: visibilityLevels,
};

export const usersTable = defineTable({
...userFields,
modifiedAt: v.optional(v.number()),
})
.index('byCountryCode', ['countryCode'])
.index('byName', ['givenName', 'familyName'])
.index('byUsername', ['username']);
export const visibilityLevels = v.union(
v.literal('hidden'),
v.literal('public'),
);

const userFields = {
email: v.string(),
avatarUrl: v.optional(v.string()),
givenName: v.optional(v.string()),
familyName: v.optional(v.string()),
countryCode: v.optional(v.string()),
username: v.string(),
nameVisibility: visibilityLevels,
locationVisibility: visibilityLevels,
};

export const usersTable = defineTable({
...userFields,
modifiedAt: v.optional(v.number()),
})
.index('byCountryCode', ['countryCode'])
.index('byName', ['givenName', 'familyName'])
.index('byUsername', ['username']);
(Thanks, by the way, for the help so far)
Gi
Gi2d ago
That does look correct, I have something similar (only difference is I return the default values in the profile callback). What can help is add an env variable in dashboard for auth debug messages AUTH_LOG_LEVEL=DEBUG it'll show a bunch of debug messages in the dashboard logs or disable schema validation and see what actually makes it into the table
ianpaschal
ianpaschalOP2d ago
Also, just to check, because it's not stated in the documentation, but I assume the CustomProfile.ts in the documentation should be added to auth.ts in place of the default Password?
import { convexAuth } from "@convex-dev/auth/server";
import Password from "./CustomProfile";

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],
});
import { convexAuth } from "@convex-dev/auth/server";
import Password from "./CustomProfile";

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],
});
As in, temporarily wrap it in v.optional()?
Gi
Gi2d ago
Yes, or disable it for the entire schema like so:
defineSchema(
{
// Define tables here.
},
{
schemaValidation: false,
},
);
defineSchema(
{
// Define tables here.
},
{
schemaValidation: false,
},
);
ianpaschal
ianpaschalOP2d ago
Let me give that a try...
Gi
Gi2d ago
You can also just do this in auth.ts
export const { auth, signIn: signInAuth, signOut, store, isAuthenticated } = convexAuth({
providers: [
Password({
validatePasswordRequirements: () => true,
profile(params, ctx) {
return {
name: params.name as string,
email: params.name as string,
...
};
}
})
],
});
export const { auth, signIn: signInAuth, signOut, store, isAuthenticated } = convexAuth({
providers: [
Password({
validatePasswordRequirements: () => true,
profile(params, ctx) {
return {
name: params.name as string,
email: params.name as string,
...
};
}
})
],
});
ianpaschal
ianpaschalOP2d ago
Right. that's a bit cleaner. So! I made everything optional, it absolutely comes into the users table. So I guess FormData is the correct approach...
Gi
Gi2d ago
the fields have values?
ianpaschal
ianpaschalOP2d ago
yup!
Gi
Gi2d ago
the custom ones
ianpaschal
ianpaschalOP2d ago
No description
ianpaschal
ianpaschalOP2d ago
However this seems to be a bug in that case...
Gi
Gi2d ago
And that table is called "users" right?
ianpaschal
ianpaschalOP2d ago
yup! It all seems to work if those fields are optional. If they are required, something goes wrong...
Gi
Gi2d ago
something like this
import { defineSchema, defineTable } from "convex/server";
import { authTables } from "@convex-dev/auth/server";
import { v } from "convex/values";

const schema = defineSchema({
...authTables,
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
// other "users" fields...
roles: v.optional(v.array(v.string()))
}).index("email", ["email"]),
// Your other tables...
});

export default schema;
import { defineSchema, defineTable } from "convex/server";
import { authTables } from "@convex-dev/auth/server";
import { v } from "convex/values";

const schema = defineSchema({
...authTables,
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
// other "users" fields...
roles: v.optional(v.array(v.string()))
}).index("email", ["email"]),
// Your other tables...
});

export default schema;
ianpaschal
ianpaschalOP2d ago
I took most of those fields out as they're neither needed or actively confusing (name instead of givenName and username), etc.
export const visibilityLevels = v.union(
v.literal('hidden'),
v.literal('public'),
);

const userFields = {
email: v.string(),
avatarUrl: v.optional(v.string()),
givenName: v.optional(v.string()),
familyName: v.optional(v.string()),
countryCode: v.optional(v.string()),
username: v.optional(v.string()),
nameVisibility: v.optional(visibilityLevels),
locationVisibility: v.optional(visibilityLevels),
};

export const usersTable = defineTable({
...userFields,
modifiedAt: v.optional(v.number()),
})
.index('byCountryCode', ['countryCode'])
.index('byName', ['givenName', 'familyName'])
.index('byUsername', ['username']);
export const visibilityLevels = v.union(
v.literal('hidden'),
v.literal('public'),
);

const userFields = {
email: v.string(),
avatarUrl: v.optional(v.string()),
givenName: v.optional(v.string()),
familyName: v.optional(v.string()),
countryCode: v.optional(v.string()),
username: v.optional(v.string()),
nameVisibility: v.optional(visibilityLevels),
locationVisibility: v.optional(visibilityLevels),
};

export const usersTable = defineTable({
...userFields,
modifiedAt: v.optional(v.number()),
})
.index('byCountryCode', ['countryCode'])
.index('byName', ['givenName', 'familyName'])
.index('byUsername', ['username']);
This is what it looks like. Which works fine, so long as all fields but email are optional. Which I guess could be a workaround right now, but the documentation seems to suggest this is not needed and in later when using the inferred types it would be nice to not have | undefined for username and the visibilities :/

Did you find this page helpful?