Convex + Better Auth

Migrate to 0.8

Migrate to @convex-dev/better-auth@0.8

To use Local Install, complete this migration guide first and then follow the Local Install guide.

Upgrade

Update the component

Update Better Auth and the component.

npm install @convex-dev/better-auth@latest better-auth@1.3.8 --save-exact

Update component instance

Replace the component instance with createClient.

convex/auth.ts
import {
  AuthFunctions,
  BetterAuth, 
  PublicAuthFunctions, 
  createClient, 
} from "@convex-dev/better-auth";
import { DataModel } from "./_generated/dataModel";
import { components } from "./_generated/api";

const authFunctions: AuthFunctions = internal.auth;
const publicAuthFunctions: PublicAuthFunctions = api.auth; 

export const betterAuthComponent = new BetterAuth(
export const authComponent = createClient<DataModel>(
  components.betterAuth, {
    authFunctions,
    publicAuthFunctions, 
  });

// These will be used in the next step
export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi() 

Convert hooks from createAuthFunctions to triggers

The previously supported onCreateUser, onUpdateUser, and onDeleteUser hooks from betterAuthComponent.createAuthFunctions() have been replaced by triggers. See the Triggers guide for more information.

convex/auth.ts
export const {
  createUser,
  deleteUser,
  updateUser,
} = betterAuthComponent.createAuthFunctions<DataModel>({
  onCreateUser: async (ctx, user) => {
    return ctx.db.insert("users", {
      email: user.email,
    });
  },
  onDeleteUser: async (ctx, userId) => {
    await ctx.db.delete(userId as Id<"users">);
  },
});

const authFunctions: AuthFunctions = internal.auth;

export const authComponent = createClient<DataModel>(
  components.betterAuth,
  {
    authFunctions,
    triggers: {
      user: {
        onCreate: async (ctx, authUser) => {
          // Any `onCreateUser` logic should be moved here
          const userId = await ctx.db.insert('users', {
            email: authUser.email,
          })
          // Instead of returning the user id, we set it to the component
          // user table manually. This is no longer required behavior, but
          // is necessary when migrating from previous versions to avoid
          // a required database migration.
          // This helper method exists solely to facilitate this migration.
          await authComponent.setUserId(ctx, authUser._id, userId)
        },
        onUpdate: async (ctx, oldUser, newUser) => {
          // Any `onUpdateUser` logic should be moved here
        },
        onDelete: async (ctx, authUser) => {
          await ctx.db.delete(authUser.userId as Id<'users'>)
        },
      },
    },
  },
)

export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi()

Move and update Better Auth config

The createAuth function should be moved to convex/auth.ts. This isn't required, but better represents where the code actually runs, colocates it with other related server side auth code, and avoids potentially writing Convex code outside of the Convex directory for some function based config properties.

The Convex database adapter is now provided through a method on the auth component, and a GenericCtx type from the component library is now used to type the createAuth ctx arg.

There is also a new optionsOnly parameter to the createAuth function. This is used to disable logging when the function is called just to generate options. This is not required, but helpful for reducing noise in logs.

Be sure to update any imports of createAuth to the new path.

convex/auth.ts
import type { GenericCtx } from './_generated/server'; 
const {
  convexAdapter, 
  type GenericCtx, 
  createClient,
} from "@convex-dev/better-auth";
import { DataModel } from "./_generated/dataModel";

export const authComponent = createClient<DataModel>(
  components.betterAuth
  // ...
)

export const createAuth = (ctx: GenericCtx) => { 
export const createAuth = (
  ctx: GenericCtx<DataModel>, 
  { optionsOnly } = { optionsOnly: false }, 
) => {
  return betterAuth({
    // ...
    database: authComponent.adapter(ctx), 
    database: convexAdapter(ctx, authComponent), 
    // When createAuth is called just to generate options, we don't want to
    // log anything
    logger: {
      disabled: optionsOnly,
    },
  })
}

Update ctx.auth.getUserIdentity() usage

The subject property of the id token, as well as the user identity object returned from ctx.auth.getUserIdentity(), was formerly the user id from the application user table. It is now the user id from the Better Auth user table. Any direct usage of the subject property should be replaced with authComponent.getAuthUser(ctx), which returns the entire Better Auth user object (formerly referred to as user metadata).

convex/messages.ts
export const listMessages = query({
  args: {},
  handler: async (ctx) => {
    const userId = (await ctx.auth.getUserIdentity())?.subject; 
    const userId = (await authComponent.getAuthUser(ctx))?.userId; 
    return ctx.db
      .query("messages")
      .withIndex("by_userId", (q) => q.eq("userId", userId))
      .collect();
  },
});

Update authComponent.getAuthUser() usage

authComponent.getAuthUser() now throws an error if the user is not found. Use authComponent.safeGetAuthUser() to match the previous behavior.

convex/auth.ts
export const getCurrentUser = query({
  args: {},
  handler: async (ctx) => {
    const  authComponent.getAuthUser(ctx);
    return authComponent.safeGetAuthUser(ctx);
  },
});
export const getCurrentUser = query({
  args: {},
  handler: async (ctx) => {
    const userMetadata = await betterAuthComponent.getAuthUser(ctx); 
    const userMetadata = await authComponent.safeGetAuthUser(ctx); 
    if (!userMetadata) {
      return null;
    }
    const user = await ctx.db.get(userMetadata.userId as Id<"users">);
    return {
      ...user,
      ...userMetadata,
    };
  },
});

Framework specific changes

If your project uses TanStack Start, follow the last few steps of the TanStack Start guide and make sure your code aligns.

This migration should not have framework specific impacts for any other framework.

That's it!

Please report any issues or inaccuracies from this guide via GitHub issues or in the #better-auth channel on Discord.

Other breaking changes

Convex plugin options removed

The named options parameter to the Convex plugin that accepted Better Auth options has been removed. This was necessary because the Convex plugin previously customized the session, breaking type inference for configuration that affected the session, so the Better Auth options had to be separated to a new function and passed in to the Convex plugin.

None of this is necessary anymore, if you were doing this you can go back to using a single createAuth function.

convex/auth.ts
const createOptions = (ctx: GenericCtx) =>
  ({
    baseURL: siteUrl,
    database: convexAdapter(ctx, betterAuthComponent),
    plugins: [
      // ...plugins
    ],
  }) satisfies BetterAuthOptions;

export const createAuth = (ctx: GenericCtx) => {
  const options = createOptions(ctx);
  return betterAuth({
    ...options,
    plugins: [...options.plugins, convex({ options })],
  });
};

export const createAuth = (ctx: GenericCtx<DataModel>) => {
  return betterAuth({
    baseURL: siteUrl,
    database: authComponent.adapter(ctx),
    plugins: [
      // ...plugins
      convex(),
    ],
  });
};