Eva
Eva4mo ago

Deleting users with Convex Auth

What's the best practice for deleting users when using Convex Auth? There aren't any examples in the docs, and removing only the row on the users table throws an error if you try to sign up with the same email.
Error: [CONVEX A(auth:signIn)] [Request ID: af708226cadf810c] Server Error
Uncaught Error: Uncaught Error: Could not update user document with ID `k174brjwcmhn3gsa85rymdcr4n6zw26q`, either the user has been deleted but their account has not, or the profile data doesn't match the `users` table schema: Update on nonexistent document ID k174brjwcmhn3gsa85rymdcr4n6zw26q
Error: [CONVEX A(auth:signIn)] [Request ID: af708226cadf810c] Server Error
Uncaught Error: Uncaught Error: Could not update user document with ID `k174brjwcmhn3gsa85rymdcr4n6zw26q`, either the user has been deleted but their account has not, or the profile data doesn't match the `users` table schema: Update on nonexistent document ID k174brjwcmhn3gsa85rymdcr4n6zw26q
12 Replies
rgz
rgz4mo ago
Use a delete function and delete by _id ctx.db.delete(userId)
Eva
EvaOP4mo ago
Yes, that's what I've done. It doesn't delete other auth rows which reference the userId
rgz
rgz4mo ago
Ah, you’ll need to query for those rows then delete the items that are referenced by the userId I can help a bit more when I get home if you haven’t sorted it.
Eva
EvaOP4mo ago
I wasn't sure if there was a built-in function to Convex Auth that would help with that since it sets up all its own tables for authAccounts, authRateLimits, authSessions etc. — there are several places to potentially look
rgz
rgz4mo ago
const tags = await ctx.db.query("tags") .withIndex("by_orgId", (q) => q.eq(("orgId"), args.id as Id<"organization">)) .filter((q) => q.eq(q.field("orgId"), args.id)).collect(); for (const tag of tags) { await ctx.db.delete(tag._id as Id<"tags">); } Here’s an example In this example, if I’m deleting an organization, I want any related tags to be deleted.
Eva
EvaOP4mo ago
Thanks! This is getting closer. If I wanted to relocate a function like this (e.g. deleteAllTagsForUser within tags.ts) and then reference that internally from another mutation, would that work?
rgz
rgz4mo ago
You would need to use an action if you’re wanting to call an internal mutation
Eva
EvaOP4mo ago
Thanks! I think I'll keep it all in the mutation for now
rgz
rgz4mo ago
Yeah I think you should be okay using mutations
Eva
EvaOP4mo ago
Always trying to prematurely optimize 😭 Thanks for your help!
rgz
rgz4mo ago
Lol I hear ya. Let me know if I can be of more help.
Sronds
Sronds2w ago
if it helps anyone else, i wrote this function which works great.
export const deleteUser = userMutation({
args: {},
handler: async ({ ctx, userId }) => {
// delete all instances of the user from the database
console.log('Deleting user with id:', userId)

// authAccounts
const authAccount = await ctx.db
.query('authAccounts')
.withIndex('userIdAndProvider', (q) => q.eq('userId', userId))
.collect()

await Promise.all(
authAccount.map(async (account) => {
await ctx.db.delete(account._id)
})
)

const authSessions = await ctx.db
.query('authSessions')
.withIndex('userId', (q) => q.eq('userId', userId))
.collect()

// loop through all sessions and delete the associated refresh tokens in the authRefreshTokens table
await Promise.all(
authSessions.map(async (session) => {
const authRefreshTokens = await ctx.db
.query('authRefreshTokens')
.withIndex('sessionId', (q) => q.eq('sessionId', session._id))
.collect()

await Promise.all(
authRefreshTokens.map(async (token) => {
await ctx.db.delete(token._id)
})
)
})
)

// delete all sessions
await Promise.all(
authSessions.map(async (session) => {
await ctx.db.delete(session._id)
})
)

// MAKE SURE TO DELETE ANY OTHER USER DATA YOU'VE CREATED YOURSELF IN OTHER TABLES (messages, posts, etc...)

// delete user from the users table
return await ctx.db.delete(userId)
},
})
export const deleteUser = userMutation({
args: {},
handler: async ({ ctx, userId }) => {
// delete all instances of the user from the database
console.log('Deleting user with id:', userId)

// authAccounts
const authAccount = await ctx.db
.query('authAccounts')
.withIndex('userIdAndProvider', (q) => q.eq('userId', userId))
.collect()

await Promise.all(
authAccount.map(async (account) => {
await ctx.db.delete(account._id)
})
)

const authSessions = await ctx.db
.query('authSessions')
.withIndex('userId', (q) => q.eq('userId', userId))
.collect()

// loop through all sessions and delete the associated refresh tokens in the authRefreshTokens table
await Promise.all(
authSessions.map(async (session) => {
const authRefreshTokens = await ctx.db
.query('authRefreshTokens')
.withIndex('sessionId', (q) => q.eq('sessionId', session._id))
.collect()

await Promise.all(
authRefreshTokens.map(async (token) => {
await ctx.db.delete(token._id)
})
)
})
)

// delete all sessions
await Promise.all(
authSessions.map(async (session) => {
await ctx.db.delete(session._id)
})
)

// MAKE SURE TO DELETE ANY OTHER USER DATA YOU'VE CREATED YOURSELF IN OTHER TABLES (messages, posts, etc...)

// delete user from the users table
return await ctx.db.delete(userId)
},
})
if anyone is curious, this is my userMutation:
export const userMutation = customMutation(
mutation,
customCtx(async (ctx) => {
const userId = await getUserIdentity(ctx)
return { userId, ctx }
})
)
export const userMutation = customMutation(
mutation,
customCtx(async (ctx) => {
const userId = await getUserIdentity(ctx)
return { userId, ctx }
})
)