#better auth <> convex

23 messages · Page 1 of 1 (latest)

hard latch
#

how to sync to convex users table from better auth upon signing up?

solid flickerBOT
#

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();

[Update userId refs]


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.

hard latch
#

@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)
solid flickerBOT
#

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:

  • authFunctions should be internal.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 internal from ./_generated/api (Convex generates this).
  • Triggers only run when Better Auth actually creates a user (e.g. via authClient.signUp / signIn flows), 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.

hard latch
#

@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, {
solid flickerBOT
#

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, AuthFunctions is the explicit type for internal.auth, so authFunctions is 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.

hard latch
#

@solid flicker im using 0.10.0

solid flickerBOT
#

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 triggers object is typed from the component schema, not your app DataModel. If you only call createClient<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.user is correctly understood as the Better Auth user table, and ctx.db inside the trigger is your normal Convex mutation ctx, so inserting into your own "users" table type-checks. [Update user triggers]

If you’re not on Local Install yet and don’t have convex/betterAuth/schema.ts, the docs don’t show a supported way to strongly type triggers without that schema; in that case, the knowledge sources don’t provide a precise typing recipe beyond “use Local Install and the generated schema.”

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: