entropy
entropy4mo ago

Best way of querying documents when using Clerk

Hello! I'm currently working on a project that uses both Convex and Clerk together. I've been running into some issues with consistency within my backend when it comes to query documents by id and wanted to see if anyone had advice. The problem is that it's easy and convenient to query by a user's Clerk id, but I lose the type safety of the _id prop. It also leads to weird situations when creating relationships between tables. Should I just my me query function (returns user info protected by auth RLS) every time I want to query with a user id or is using the clerkId as the primary way of fetching a document a ok practice? me function
export const me = authRLSQuery({
handler: async ctx => {
const user = await getUser(ctx, ctx.identity.subject)
if (!user) {
throw new ConvexError({ message: 'No user found for this identifier' })
}
return user
}
})
export const me = authRLSQuery({
handler: async ctx => {
const user = await getUser(ctx, ctx.identity.subject)
if (!user) {
throw new ConvexError({ message: 'No user found for this identifier' })
}
return user
}
})
utils
export async function getUser(ctx: QueryCtx | MutationCtx, clerkId: string) {
const user = await ctx.db
.query('users')
.withIndex('by_clerkId', q => q.eq('clerkId', clerkId))
.first()
return user
}
export const authRLSQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new ConvexError({
code: 'UNAUTHORIZED',
message: 'Sorry, you must be logged in to perform this action'
})
}

const db = wrapDatabaseReader({ identity }, ctx.db, {
users: {
read: async (ctx, user) => ctx.identity.subject === user.clerkId
}
})
return { ctx: { ...ctx, db, identity }, args }
}
})
export async function getUser(ctx: QueryCtx | MutationCtx, clerkId: string) {
const user = await ctx.db
.query('users')
.withIndex('by_clerkId', q => q.eq('clerkId', clerkId))
.first()
return user
}
export const authRLSQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new ConvexError({
code: 'UNAUTHORIZED',
message: 'Sorry, you must be logged in to perform this action'
})
}

const db = wrapDatabaseReader({ identity }, ctx.db, {
users: {
read: async (ctx, user) => ctx.identity.subject === user.clerkId
}
})
return { ctx: { ...ctx, db, identity }, args }
}
})
4 Replies
ballingt
ballingt4mo ago
The reason you don't want to rely on a me query on the client side is that then you're writing functions that rely on passing in the user id directly and that's not safe: a user could pass in whatever they wanted from the client. Could you say more about losing type safety of the _id prop? _id isn't much different than any other property, so you can make your own property that refers to a Clerk user id.
entropy
entropyOP4mo ago
So in that case I should use ctx.identity.subject passed from my RLS and then query the user for their user id? The main thing is it feels a bit weird having two main id fields with the clerkId field allowing my to avoid querying the user for their id. For example if I have a relationship like this where an entry belongs to a user I could either: one use the _id field of the user but then that means I have to query a user by their clerk id to get their user id, or two use the clerk id as the field for relationship and always query by the clerk id. I feel like either there's something I'm missing or there's a better way.
ballingt
ballingt4mo ago
Either one of these is fine: using the clerk id as a foreign key to find entities that belong to a user, or using the clerk id as an indexed field on a Users table where you store other things, including a _id that is the foreign key in your other entities. If you go with the seperate user record approach, probably I'd do this get this User record in the authRLSQuery wrapper, so instead of or in addition to ctx.identity you add ctx.currentUser or similar
entropy
entropyOP4mo ago
I see that makes sense and thank you for the insight! as a quick example something like this would be best if I decided to go the seperate user route?
export const getByUser = authRLSQuery({
args: { mediaId: union(id('anime')) },
handler: async (ctx, args) => {
return await ctx.db
.query('entries')
.withIndex('by_userId_mediaId', q =>
q.eq('userId', ctx.user._id).eq('mediaId', args.mediaId)
)
.first()
}
})
export const getByUser = authRLSQuery({
args: { mediaId: union(id('anime')) },
handler: async (ctx, args) => {
return await ctx.db
.query('entries')
.withIndex('by_userId_mediaId', q =>
q.eq('userId', ctx.user._id).eq('mediaId', args.mediaId)
)
.first()
}
})
export const authRLSQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new ConvexError({
code: 'UNAUTHORIZED',
message: 'Sorry, you must be logged in to perform this action'
})
}

const user = await getUser(ctx, identity.subject)
if (!user) {
throw new ConvexError({ message: 'No user found for this identifier.' })
}

const db = wrapDatabaseReader({ identity }, ctx.db, {
users: {
read: async (ctx, user) => ctx.identity.subject === user.clerkId
}
})
return { ctx: { ...ctx, db, identity, user }, args }
}
})
export const authRLSQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new ConvexError({
code: 'UNAUTHORIZED',
message: 'Sorry, you must be logged in to perform this action'
})
}

const user = await getUser(ctx, identity.subject)
if (!user) {
throw new ConvexError({ message: 'No user found for this identifier.' })
}

const db = wrapDatabaseReader({ identity }, ctx.db, {
users: {
read: async (ctx, user) => ctx.identity.subject === user.clerkId
}
})
return { ctx: { ...ctx, db, identity, user }, args }
}
})

Did you find this page helpful?