Llabe
Llabe•2w ago

convex auth user creation

For what I saw in some videos, the user should get created automatically when using convex auth. But in my case its not happening, does anyone know why it could be?
47 Replies
Convex Bot
Convex Bot•2w 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!
ballingt
ballingt•2w ago
Debugging - Convex Auth
Authentication library for your Convex backend
ballingt
ballingt•2w ago
It's hard to guess what's wrong without knowing how you set up Convex Auth, what your code looks like etc. Is there a guide you followed to set up Convex Auth?
ballingt
ballingt•2w ago
It's easy to miss some steps in https://labs.convex.dev/auth
Convex Auth - Convex Auth
Authentication library for your Convex backend
Karolus
Karolus•6d ago
I faced the same error just know, my schema:
import { defineSchema } from "convex/server";
import { authTables } from "@convex-dev/auth/server";

const schema = defineSchema({
...authTables,
// Your other tables...
});

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

const schema = defineSchema({
...authTables,
// Your other tables...
});

export default schema;
No description
Karolus
Karolus•6d ago
also hit another issue: 1. Type '(githubProfile: GitHubProfile) => { id: number; name: string | null; email: string | null; image: unknown; githubId: number; }' is not assignable to type 'ProfileCallback<GitHubProfile>'. Type '{ id: number; name: string | null; email: string | null; image: unknown; githubId: number; }' is not assignable to type 'Awaitable<User>'. Type '{ id: number; name: string | null; email: string | null; image: unknown; githubId: number; }' is not assignable to type 'User'. Types of property 'id' are incompatible. Type 'number' is not assignable to type 'string'. [2322] in:
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
GitHub({
profile(githubProfile) {
return {
id: githubProfile.id,
name: githubProfile.name,
email: githubProfile.email,
image: githubProfile.picture,
githubId: githubProfile.id,
};
},
}),
],
});
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
GitHub({
profile(githubProfile) {
return {
id: githubProfile.id,
name: githubProfile.name,
email: githubProfile.email,
image: githubProfile.picture,
githubId: githubProfile.id,
};
},
}),
],
});
@Tom any idea? I am confused as schema of authTable allows optional email:

users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
...

users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
...
ballingt
ballingt•6d ago
@Karolus This validator error says email was null, which isn't allowed by v.optional(v.string()). Only the property missing is allowed. @Karolus do you see how to fix the second error? It says that what you're passing as the id prop, githubProfile.id, is a number, but id expects a string. So you should convert it into a string
Karolus
Karolus•6d ago
regarding second issue, yes you can cast it as unknown as string, regarding first issue I am not really sure why I am not passing email, everything seems to work (redirect to github, authorization and redirect to my app)
ballingt
ballingt•6d ago
Don't do that, as unknown as string just changes the TypeScript type. It doesn't change the actual type Try "" + githubProfile.id, that's a way to actually convert it to a string id: "" + githubProfile.id,
Karolus
Karolus•6d ago
the type is actually provided by convex not mine
ballingt
ballingt•6d ago
The type of id needs to be a string for Convex Auth. But you're passing a number, because githubProfile.id is a number. You should convert this number into a string
Karolus
Karolus•6d ago
got it, although even without returning values the problem with email remains:
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub],
});
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub],
});
ballingt
ballingt•6d ago
The first problem, right?
Karolus
Karolus•6d ago
yes
ballingt
ballingt•6d ago
What user are you seeing this behavior for, your own?
Karolus
Karolus•6d ago
I tried 2 accounts, for both actually
ballingt
ballingt•6d ago
Do you have "share public email" disabled for these accounts? in GitHub settings? This sounds a bit like https://github.com/nextauthjs/next-auth/issues/374
Karolus
Karolus•6d ago
probably
ballingt
ballingt•6d ago
Are you using a GitHub OAuth app or a GitHub App?
ballingt
ballingt•6d ago
Karolus
Karolus•6d ago
Oh man, I would have never guessed that's where the problem is 😄 Should be OAuth Apps, right?
ballingt
ballingt•6d ago
Yeah, apparently GitHub Apps work mostly but email may be null That GitHub issue linked above has potential solutions for using a GitHub App instead, but we've never tested this I've only tried GitHub OAuth apps
Karolus
Karolus•6d ago
thanks, I will try with OAuth Apps then confirmed, it worked! Thanks Tom one last question, when using TanStack Router (not TanStack Start)
import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router";
import { useConvexAuth } from "convex/react";

export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});

function AuthLayout() {
const auth = useConvexAuth();
console.log(auth);

const { isAuthenticated, isLoading } = auth;

if (isLoading) {
return <div>Loading authentication...</div>;
}

if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}

return <Outlet />;
}
import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router";
import { useConvexAuth } from "convex/react";

export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});

function AuthLayout() {
const auth = useConvexAuth();
console.log(auth);

const { isAuthenticated, isLoading } = auth;

if (isLoading) {
return <div>Loading authentication...</div>;
}

if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}

return <Outlet />;
}
That would be best convention for auth? or this:
import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router";
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";

export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});

function AuthLayout() {
return (
<>
<AuthLoading>
<div>Loading authentication...</div>
</AuthLoading>
<Unauthenticated>
<Navigate to="/login" replace />
</Unauthenticated>
<Authenticated>
<Outlet />
</Authenticated>
</>
);
}
import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router";
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";

export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});

function AuthLayout() {
return (
<>
<AuthLoading>
<div>Loading authentication...</div>
</AuthLoading>
<Unauthenticated>
<Navigate to="/login" replace />
</Unauthenticated>
<Authenticated>
<Outlet />
</Authenticated>
</>
);
}
ballingt
ballingt•6d ago
These two should be the same, the implementation of <AuthLoading> and <Unauthenticated> and <Authenticated> is just using that useConvexAuth hook
Karolus
Karolus•6d ago
I am asking because TanStack router recommends this type of solution:
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
if (!isAuthenticated()) {
throw redirect({
to: '/login',
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
})
}
},
})
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
if (!isAuthenticated()) {
throw redirect({
to: '/login',
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
})
}
},
})
ballingt
ballingt•6d ago
I think these look good, let me know if you run into anything, I'd like to add a little guide somewhere for this Maybe to https://github.com/TanStack/router/tree/main/examples/react or to https://docs.convex.dev/home somewhere @Karolus mind if I use your screenshot to open an issue at https://github.com/get-convex/convex-auth/issues? will be easier to find if people search for it
Karolus
Karolus•6d ago
sure regarding convex auth with TanStack Router the closest I found was Clerk implementation, you can can auth state from context like so:
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context }) => {
if (!context.userId) { // here
throw new Error('Not authenticated')
}
},
errorComponent: ({ error }) => {
if (error.message === 'Not authenticated') {
return (
<div className="flex items-center justify-center p-12">
<SignIn routing="hash" forceRedirectUrl={window.location.href} />
</div>
)
}

throw error
},
})
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context }) => {
if (!context.userId) { // here
throw new Error('Not authenticated')
}
},
errorComponent: ({ error }) => {
if (error.message === 'Not authenticated') {
return (
<div className="flex items-center justify-center p-12">
<SignIn routing="hash" forceRedirectUrl={window.location.href} />
</div>
)
}

throw error
},
})
but convexClient seems to don't have auth property
ballingt
ballingt•6d ago
Did you see this TanStack Start + Convex + Clerk example? https://github.com/get-convex/templates/tree/main/template-tanstack-start-clerk oh and https://docs.convex.dev/client/react/tanstack-start/tanstack-start-with-clerk I know you're not using Start but it has some of the context stuff
Karolus
Karolus•6d ago
exactly this one, that's from I took above example
Karolus
Karolus•6d ago
I am even passing convexClient to context
No description
Karolus
Karolus•6d ago
but I don't think it has auth state, right?
ballingt
ballingt•6d ago
great i opened https://github.com/get-convex/convex-auth/issues/161 documenting the GitHub OAuth thing ah I see hm yeah, I see we need to do auth at that level somehow right now the Convex Auth handshakes are being driven by that React component, but we need it in loaders (client and server) too This has been on my list for a bit but because TanStack Start has take a while to get to 1.0 I haven't done it yet So the goal would be similar to our Convex Auth Next.js middlware, that you can use a cookie or local storage to auth in beforeLoad like the clerk example does and then you already know if you're authed ahead of time. But a bit tricky because there are two steps in Convex Auth, 1) getting the token and 2) actually sending it in over the WebSocket so we'd also want to create the ConvexClient sooner, and at least send the token over the websocket before going further I don't think we have to wait for the auth response, but we could via convexClient.setAuth(getTokenFunction, onChange)
Karolus
Karolus•6d ago
I don't think it matters in my particular case as I am wrapping this in Tauri app (which doesn't supports SSR) not sure how exactly beforeLoad works under the hood that being said Clerk, Supabase examples uses beforeLoad for auth: Supabase example
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context }) => {
if (!context.user) {
throw new Error('Not authenticated')
}
},
errorComponent: ({ error }) => {
if (error.message === 'Not authenticated') {
return <Login />
}

throw error
},
})
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context }) => {
if (!context.user) {
throw new Error('Not authenticated')
}
},
errorComponent: ({ error }) => {
if (error.message === 'Not authenticated') {
return <Login />
}

throw error
},
})
https://tanstack.com/start/latest/docs/framework/react/examples/start-supabase-basic
ballingt
ballingt•6d ago
For Convex we probably need both, since Convex involves contantly refreshing the auth tokens while the user is on the page. But that works fine for the Clerk + Convex example, I just need to add some functionality for doing this with Convex Auth we can do the wrap automatically too, that's what we do with the ConvexProvider
Karolus
Karolus•6d ago
I see, so we would still need to wrap <Outlet /> with <Authenticated /> to refresh the auth token?
Actually TanStack Router is client side router so beforeLoad also runs on client side from my understanding, so I think this approach could also work with Taurii technically. The only thing I am noticing in this solution:
import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router";
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";

export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});

function AuthLayout() {
return (
<>
<AuthLoading>
<div>Loading authentication...</div>
</AuthLoading>
<Unauthenticated>
<Navigate to="/login" replace />
</Unauthenticated>
<Authenticated>
<Outlet />
</Authenticated>
</>
);
}
import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router";
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";

export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});

function AuthLayout() {
return (
<>
<AuthLoading>
<div>Loading authentication...</div>
</AuthLoading>
<Unauthenticated>
<Navigate to="/login" replace />
</Unauthenticated>
<Authenticated>
<Outlet />
</Authenticated>
</>
);
}
Loading authentication... is visible after every page refresh. So there is always this 1s delay before authentication state is determined.
ballingt
ballingt•6d ago
Can you do this part in a layout that's shared between all these paths? oh wait ever refresh, or every navigation? re refresh, yeha we'll need the beforeLoad solution for that I think
Karolus
Karolus•5d ago
Great @Tom ,thanks for the help again. I am looking forward to the improvements 🙂 @Tom Actually, it happens on every navigation as well. I tested both <Authenticated> and useContextAuth; they both result in this 1s delay to determine the auth state.
export const Route = createFileRoute('/_authenticated')({
component: AuthetnicatedLayoutRoute,
});

function AuthetnicatedLayoutRoute() {
const { isAuthenticated, isLoading } = useConvexAuth();
const navigate = useNavigate();

useEffect(() => {
// Redirect to login page if user is not authenticated.
if (!isLoading && !isAuthenticated) {
navigate({ to: '/signin' });
}
}, [isLoading, isAuthenticated]);

if (isLoading && !isAuthenticated) {
return <Loader />; // Every page navigation, I see this loader for 1-2 seconds before isAuthenticated is determined
}

return (
<SidebarProvider>
<AppSidebar />
<main>
<SidebarTrigger />
<Outlet />
</main>
</SidebarProvider>
);
}
export const Route = createFileRoute('/_authenticated')({
component: AuthetnicatedLayoutRoute,
});

function AuthetnicatedLayoutRoute() {
const { isAuthenticated, isLoading } = useConvexAuth();
const navigate = useNavigate();

useEffect(() => {
// Redirect to login page if user is not authenticated.
if (!isLoading && !isAuthenticated) {
navigate({ to: '/signin' });
}
}, [isLoading, isAuthenticated]);

if (isLoading && !isAuthenticated) {
return <Loader />; // Every page navigation, I see this loader for 1-2 seconds before isAuthenticated is determined
}

return (
<SidebarProvider>
<AppSidebar />
<main>
<SidebarTrigger />
<Outlet />
</main>
</SidebarProvider>
);
}
ballingt
ballingt•5d ago
I would hope that for navigations, the AuthentticatedLayoutRoute would remain mounted so there's be no need to redo this so sounds like we have some work to do, either an example of where to mount this differently or better, the beforeLoad approach
Karolus
Karolus•5d ago
Yeah, seems like useConvexAuth() is at the component level, so it runs again for every navigation. I could probably store the auth state locally in Zustand and rely on it, but it's a bit of a stretch.
ballingt
ballingt•5d ago
useConvexAuth() has very little logic, it just grabs a context from an ancestor higher up the React element tree so that higher-up thing is probably what's getting remounted, or otherwise causing a reset https://github.com/get-convex/convex-js/blob/36d4a37e4b90ddcd4717e35572b8931331b7e23d/src/react/ConvexAuthState.tsx#L33-L58
Karolus
Karolus•5d ago
I see, unfortunately, it causes anything that was already mounted to disappear for 1-2 seconds, like AppSidebar in my example Tomorrow, I will switch to Clerk for auth and verify if beforeLoad fixes the issue.
Karolus
Karolus•5d ago
ah I see in docs when u use Clerk with Convex you still recommend to use useConvexAuth hook, so I will face same issue there
No description
ballingt
ballingt•5d ago
useConvexAuth isn't the problem here, it returns whatever the auth state is from higher in the react tree
Karolus
Karolus•5d ago
Yeah, got it. Just pointing that the same problem will existing no matter if it's Convex Auth or Clerk + Convex. It's a pity, I really enjoyed setting up the Auth with Convex. Super simple, good docs, but because of that issue, I don't think it's usable at this moment, at least not in Tanstack Router.
ballingt
ballingt•5d ago
Try Clerk, I don't think it has this issue?
Karolus
Karolus•4d ago
hm I will try, but are you sure? Clerk integration also relies on the same logic.: https://docs.convex.dev/auth/clerk#logged-in-and-logged-out-views but Provider is different so maybe re-mounting won't happen https://discord.com/channels/1019350475847499849/1335363286123675680
ballingt
ballingt•2d ago
We followed up in the TanStack Discord, turns out these were full page navigations so the auth had to be brought up again. Things are working correctly with client-side navigations, so <Link to={url}> instead of <a>

Did you find this page helpful?