Component user id in app table
Migrate user id reference from component to app table
This guide shows one of two recommended strategies for migrating away from
maintaining a user.userId field in the Better Auth user table. Read the
overview for more information.
The migration guide covers basic required changes, but your application may have additional or altered requirements depending on how the Better Auth component is implemented. Be sure to review any auth related code for potential impacts. Avoid type assertions and checking for type errors is encouraged.
This guide is for migrating from tracking the app user id in the component to
tracking the component user id in the app user table via an authId field.
Install migrations component
Install and configure the migrations component.
npm install @convex-dev/migrationsimport { defineApp } from "convex/server";
import betterAuth from "@convex-dev/better-auth/convex.config";
import migrations from "@convex-dev/migrations/convex.config";
const app = defineApp();
app.use(betterAuth);
app.use(migrations);
export default app;Add authId field to users table
Add an authId field to your app users table. You can use any name you want,
but keep in mind that this guide will refer to it as authId. The field should
be optional for now, it can be changed to required later.
export default defineSchema({
users: defineTable({
email: v.string(),
authId: v.optional(v.string()),
})
.index("email", ["email"])
.index("authId", ["authId"]),
// ...
});Add migration functions
Create a convex/migrations.ts file and add migration functions. Two functions
are added - one for adding the authId field to the app users table, and one for
removing the userId field from the related Better Auth user. We'll run them in
later steps.
import { Migrations } from "@convex-dev/migrations";
import { components, internal } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { authComponent } from "./auth";
export const migrations = new Migrations<DataModel>(components.migrations);
// Define migration functions
export const migrationAddAuthId = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
// For each user in the app users table, get the related Better Auth user id
// and set it to the `authId` field.
if (user.authId === undefined) {
const authUser = await authComponent.migrationGetUser(ctx, user._id);
if (!authUser) {
throw new Error(`Auth user not found for id ${user._id}`);
}
await ctx.db.patch(user._id, { authId: authUser._id });
}
},
});
export const migrationRemoveUserId = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
// For each user in the app users table, remove the `userId` value from the
// related Better Auth user
await authComponent.migrationRemoveUserId(ctx, user._id);
},
});
// Export runnable migration functions
export const addAuthId = migrations.runner(
internal.migrations.migrationAddAuthId
);
export const removeUserId = migrations.runner(
internal.migrations.migrationRemoveUserId
);Update adapter exports (Local Install only)
If using Local Install, update the adapter exports to include the migrationRemoveUserId function.
export const {
create,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
migrationRemoveUserId,
} = createApi(schema, createAuth);Update user triggers
Update user triggers to set the user authId field on creation, and use it to
get the app user on update and delete.
Depending on how you're using Better Auth, you may or may not be using all of these triggers.
export const authComponent = createClient<DataModel, typeof betterAuthSchema>(
components.betterAuth,
{
authFunctions,
triggers: {
user: {
onCreate: async (ctx, authUser) => {
const userId = await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id,
});
await authComponent.setUserId(ctx, authUser._id, userId);
},
onUpdate: async (ctx, authUser, prevAuthUser) => {
// Example trigger logic for syncing email address, your logic
// and details may differ
if (authUser.email !== prevAuthUser.email) {
const user = await ctx.db.query("users")
.withIndex("authId", (q) => q.eq("authId", authUser._id))
.unique();
if (!user) {
throw new ConvexError('User not found')
}
await ctx.db.patch(authUser.authId as Id<'users'>, {
await ctx.db.patch(user._id, {
email: authUser.email,
})
}
},
onDelete: async (ctx, authUser) => {
// Example logic, your logic and details may differ
await ctx.db.delete(authUser.userId as Id<'users'>)
const user = await ctx.db.query("users")
.withIndex("authId", (q) => q.eq("authId", authUser._id))
.unique();
if (!user) {
throw new ConvexError('User not found')
}
await ctx.db.delete(authUser.userId as Id<'users'>)
await ctx.db.delete(user._id)
},
// ...
},
},
}
);Run addAuthId migration
At this point, with the convex dev server running, your code is still
functioning as it did prior to starting this guide, but is now also defining
authId on every new user, so all new users moving forward will have an
authId value.
This migration will backfill the authId field for previously existing
users in your development deployment.
npx convex run migrations:addAuthIdDeploy to production
Before proceeding, make sure the migration was successful in your development
deployment by running your application locally. Also confirm that the authId
field on all users in your development deployment app user table is set.
The changes made so far, which are strictly additive and not destructive, should be deployed to production. The first migration will need to run there as well.
Deployment steps depend on how you deploy your app. Most apps deploy through Netlify or Vercel by merging changes to the main branch of their project repo.
Run addAuthId migration in production
With production successfully deployed, the migration can be run against the
production deployment. This can happen through the production deployment
dashboard, or through the cli using the --prod flag.
npx convex run migrations:addAuthId --prodMake authId required
Now that all authId values are set, make the authId field required in the
app user table. If the convex dev server errors after this change, you probably
have one or more users in the app user table that don't have an authId value.
This should only be possible for users that do not have a related Better Auth user.
export default defineSchema({
users: defineTable({
email: v.string(),
authId: v.optional(v.string()),
authId: v.string(),
}).index("email", ["email"]),
});Remove userId writes
Update the user onCreate trigger to stop setting the userId field on the Better Auth user.
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions,
triggers: {
user: {
onCreate: async (ctx, authUser) => {
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id,
});
await authComponent.setUserId(ctx, authUser._id, userId);
},
},
},
});Update userId references
Any references in your codebase to the userId field from the Better Auth user
should be updated to reference the authId field in the app user table. Your
app may not have any such references.
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
const authUser = await authComponent.safeGetAuthUser(ctx);
if (!authUser) {
return;
}
const user = await ctx.db.get(authUser.userId as Id<"users">);
const user = await ctx.db
.query("users")
.withIndex("authId", (q) => q.eq("authId", authUser._id))
.unique();
if (!user) {
return;
}
return { ...user, ...withoutSystemFields(authUser) };
},
});Run removeUserId migration
The migration is now functionally complete, all that remains are unused
userId values in the Better Auth user table.
Run the removeUserId migration to clear the unused values.
npx convex run migrations:removeUserIdDeploy to production
After ensuring your application works after these changes, and that the
userId field is no longer being set on the Better Auth user table, changes
should be deployed to production.
Run removeUserId migration in production
Once deployed, as we did previously, the second migration should also be run in production.
npx convex run migrations:removeUserId --prodRemove migration code
Remove the migration dependencies and code added previously.
Only remove the migration component if not used elsewhere in your project.
npm uninstall @convex-dev/migrationsimport { defineApp } from "convex/server";
import betterAuth from "@convex-dev/better-auth/convex.config";
import migrations from "@convex-dev/migrations/convex.config";
const app = defineApp();
app.use(betterAuth);
app.use(migrations);
export default app;import { Migrations } from "@convex-dev/migrations";
import { components, internal } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { authComponent } from "./auth";
export const migrations = new Migrations<DataModel>(components.migrations);
// Define migration functions
export const migrationAddAuthId = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
// For each user in the app users table, get the related Better Auth user id
// and set it to the `authId` field.
if (user.authId === undefined) {
const authUser = await authComponent.migrationGetUser(ctx, user._id);
if (!authUser) {
throw new Error(`Auth user not found for id ${user._id}`);
}
await ctx.db.patch(user._id, { authId: authUser._id });
}
},
});
export const migrationRemoveUserId = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
// For each user in the app users table, remove the `userId` value from the
// related Better Auth user
await authComponent.migrationRemoveUserId(ctx, user._id);
},
});
// Export runnable migration functions
export const addAuthId = migrations.runner(
internal.migrations.migrationAddAuthId
);
export const removeUserId = migrations.runner(
internal.migrations.migrationRemoveUserId
);export const {
create,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
migrationRemoveUserId,
} = createApi(schema, createAuth);Migration complete 🎉
You've successfully migrated your user table foreign key from Better Auth into your app user table.