#better auth <> convex
23 messages · Page 1 of 1 (latest)
Hi @hard latch. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
You sync Better Auth users into your own Convex "users" table by using Better Auth triggers in your authComponent configuration. On user creation, insert into your app table and store a reference (either app user id on the auth user, or auth user id on the app user).
There are two recommended patterns in the docs:
1. Store authId in your Convex users table (recommended for 0.9+)
Add an authId field to your users table and set it in the user.onCreate trigger. [User id in app; Update triggers]
convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
email: v.string(),
authId: v.optional(v.string()),
})
.index("email", ["email"])
.index("authId", ["authId"]),
// ...
});
convex/auth.ts
import { createClient } from "@convex-dev/better-auth";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { authFunctions } from "./somewhere"; // internal.auth, etc.
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions,
triggers: {
user: {
onCreate: async (ctx, authUser) => {
// This runs whenever a Better Auth user is created (sign up)
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id,
});
},
// onUpdate / onDelete optional
},
},
});
Now every Better Auth signup automatically creates a row in your Convex users table, linked by authId.
To look up the app user from an auth user later:
const authUser = await authComponent.safeGetAuthUser(ctx);
if (!authUser) return;
const user = await ctx.db
.query("users")
.withIndex("authId", q => q.eq("authId", authUser._id))
.unique();
2. Store app user id on the Better Auth user (legacy-compatible)
If you prefer to keep user.userId on the Better Auth side, you can add that field via additionalFields and set it in the trigger. [Component user id]
convex/auth.ts – Better Auth config
import { betterAuth } from "better-auth";
import type { GenericCtx } from "@convex-dev/better-auth";
export const createAuth = (
ctx: GenericCtx<DataModel>,
{ optionsOnly } = { optionsOnly: false }
) => {
return betterAuth({
user: {
additionalFields: {
userId: {
type: "string",
required: false,
},
},
},
});
};
Regenerate the Better Auth schema, then define a mutation to set userId and call it from the trigger: [Component user id]
convex/betterAuth/auth.ts
import { mutation } from "convex/server";
import { v } from "convex/values";
export const setUserId = mutation({
args: {
authId: v.id("user"),
userId: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.authId, {
userId: args.userId,
});
},
});
**convex/auth.ts – trigger**
```ts
export const authComponent = createClient<DataModel, typeof betterAuthSchema>(
components.betterAuth,
{
authFunctions,
local: {
schema: betterAuthSchema,
},
triggers: {
user: {
onCreate: async (ctx, authUser) => {
const userId = await ctx.db.insert("users", {
email: authUser.email,
});
await ctx.runMutation(components.betterAuth.auth.setUserId, {
authId: authUser._id,
userId,
});
},
},
},
}
);
If you share your current convex/auth.ts and schema, I can show the minimal diff to wire this up.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@hard latch Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@solid flicker
explain where is export const authComponent = createClient<DataModel>(components.betterAuth, { authFunctions,
the authFunctions above?
my code below no logs
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions: {},
triggers: {
user: {
onCreate: async (ctx, authUser) => {
console.log("🚀 ~ authUser:", authUser)
Let me check the knowledge base for you again. One moment! :books:
authFunctions is not something you invent yourself – it should be the typed object of internal Convex functions that the Better Auth component uses for its own internal RPCs.
In the docs it’s created like this (note the internal import): [Migrate to 0.8; Triggers]
// convex/auth.ts
import { components, internal } from "./_generated/api";
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
import type { DataModel } from "./_generated/dataModel";
const authFunctions: AuthFunctions = internal.auth;
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions,
triggers: {
user: {
onCreate: async (ctx, authUser) => {
console.log("🚀 ~ authUser:", authUser);
// your sync logic here
},
},
},
});
A few key points:
authFunctionsshould beinternal.auth, not{}. Passing{}means the component can’t call its internal functions, and triggers may never fire. [Migrate to 0.8]- Make sure you import
internalfrom./_generated/api(Convex generates this). - Triggers only run when Better Auth actually creates a user (e.g. via
authClient.signUp/signInflows), not when you start the dev server.
If you update your code to match the pattern above and still see no logs, the next thing to check is that a real signup is happening and that you’re using the Better Auth component (not some older setup).
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@hard latch Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@solid flicker are u sure?
To ignore failing typecheck, use `--typecheck=disable`.
convex/auth.ts:13:7 - error TS7022: 'authFunctions' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
13 const authFunctions = internal.auth;
~~~~~~~~~~~~~
convex/auth.ts:17:14 - error TS7022: 'authComponent' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
17 export const authComponent = createClient<DataModel>(components.betterAuth, {
Let me check the knowledge base for you again. One moment! :books:
Those errors are expected with the naive const authFunctions = internal.auth; form, and the docs show how to avoid them by adding an explicit type annotation and importing AuthFunctions from @convex-dev/better-auth. [Migrate to 0.8; Triggers]
Use this pattern instead:
// convex/auth.ts
import { components, internal } from "./_generated/api";
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
import type { DataModel } from "./_generated/dataModel";
const authFunctions: AuthFunctions = internal.auth;
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions,
triggers: {
user: {
onCreate: async (ctx, authUser) => {
console.log("🚀 ~ authUser:", authUser);
// sync to your users table here
},
},
},
});
Why this fixes your error:
- The TS error you’re seeing (
implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer) is the same “circular inference” issue Convex documents for actions/queries. The fix there is also to add an explicit type so TS doesn’t try to infer through a cycle. [Circular inference] - Here,
AuthFunctionsis the explicit type forinternal.auth, soauthFunctionsis no longer inferred and the cycle is broken. [Migrate to 0.8]
Also, don’t pass authFunctions: {} – that disables the internal wiring Better Auth needs and your triggers may never run. Use the internal.auth pattern above.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@hard latch Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@solid flicker im using 0.10.0
Let me check the knowledge base for you again. One moment! :books:
For @convex-dev/better-auth@0.10.0 you do not need to (and should not) pass authFunctions at all – that was part of the older 0.8 migration docs. In the 0.10 + current framework guides, createClient is called without authFunctions. [React guide; TanStack guide]
Use this pattern instead:
// convex/auth.ts
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";
import { betterAuth, type BetterAuthOptions } from "better-auth/minimal";
import authConfig from "./auth.config";
const siteUrl = process.env.SITE_URL!;
// Component client – no authFunctions here
export const authComponent = createClient<DataModel>(components.betterAuth, {
triggers: {
user: {
onCreate: async (ctx, authUser) => {
console.log("🚀 ~ authUser:", authUser);
// e.g. sync into your own users table:
// await ctx.db.insert("users", {
// email: authUser.email,
// authId: authUser._id,
// });
},
},
},
});
// Standard 0.10 createAuth
export const createAuth = (ctx: GenericCtx<DataModel>) => {
return betterAuth({
baseURL: siteUrl,
database: authComponent.adapter(ctx),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
plugins: [
convex({
authConfig,
jwksRotateOnTokenGenerationError: true,
}),
],
});
};
// Optional helper
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
return authComponent.getAuthUser(ctx);
},
});
So for 0.10.0:
- Remove `authFunctions: {}` entirely.
- Just pass `triggers` in the second argument to `createClient`.
- Make sure signups are going through Better Auth (e.g. `authClient.signUp`); then your `onCreate` trigger will run and you should see your `console.log`.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
@hard latch Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Let me check the knowledge base for you again. One moment! :books:
You’re running into a type mismatch between your app DataModel and the Better Auth component schema. The error text shows that your triggers.user is being typed against your app’s users table, not the Better Auth user table.
From the docs, when you use Better Auth with Local Install (or any time you want typed access to the Better Auth tables), you must pass the Better Auth schema as the second generic to createClient, and then define triggers against that schema. [Local install; User triggers]
The pattern looks like this:
// convex/auth.ts
import { createClient } from "@convex-dev/better-auth";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
// IMPORTANT: import the Better Auth schema generated under convex/betterAuth
import betterAuthSchema from "./betterAuth/schema";
export const authComponent = createClient<DataModel, typeof betterAuthSchema>(
components.betterAuth,
{
triggers: {
// This "user" is the Better Auth user table from betterAuthSchema,
// not your app's "users" table.
user: {
onCreate: async (ctx, authUser) => {
console.log("🚀 ~ authUser:", authUser);
// Now you can safely sync into your own app users table:
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id,
});
},
},
},
}
);
Key points tied to the error you’re seeing:
- The
triggersobject is typed from the component schema, not your appDataModel. If you only callcreateClient<DataModel>(components.betterAuth, { triggers: ... })without the Better Auth schema, TS tries to unify the trigger types with your app tables, which produces the long “ExtractFieldPaths” error you pasted. [Local install config] - With
createClient<DataModel, typeof betterAuthSchema>(...),triggers.useris correctly understood as the Better Authusertable, andctx.dbinside the trigger is your normal Convex mutation ctx, so inserting into your own"users"table type-checks. [Update user triggers]