Skip to content

Convex plugin silently drops custom JWT claims when definePayload is async #335

Description

@agucova

Summary

The convex plugin's wrapper around jwt.definePayload spreads the user-supplied function's return value synchronously, even though the type signature allows an async function. When a consumer returns a Promise, the spread yields nothing and all custom JWT claims are silently dropped and only the plugin's own sessionId and iat survive.

Reproduction

convex({
  jwt: {
    definePayload: async ({ user }) => {
      const roles = await ctx.runQuery(api.auth.getRoles, { userId: user.id });
      return { roles };
    },
  },
})

After sign-in, the decoded JWT contains only sessionId, iat, sub, exp, iss, aud.roles is missing. Convex logs show [WARN] 1 unawaited operation: [query] because the inner async work is never awaited.

Root cause

src/plugins/convex/index.ts (same in dist/plugins/convex/index.js of 0.11.4):

definePayload: ({ user, session }) => ({
    ...(opts.jwt?.definePayload
        ? opts.jwt.definePayload({ user, session })   // not awaited
        : omit(user, ["id", "image"])),
    sessionId: session.id,
    iat: Math.floor(new Date().getTime() / 1000),
}),

Meanwhile the type signature explicitly allows async (Promise<Record<string, any>> | Record<string, any>).

Proposed fix

-      definePayload: ({ user, session }) => ({
+      definePayload: async ({ user, session }) => ({
         ...(opts.jwt?.definePayload
-          ? opts.jwt.definePayload({ user, session })
+          ? await opts.jwt.definePayload({ user, session })
           : omit(user, ["id", "image"])),
         sessionId: session.id,
         iat: Math.floor(new Date().getTime() / 1000),
       }),

Sync definePayload still works, awaiting a non-Promise is a no-op. PR to follow.

Environment

  • @convex-dev/better-auth: 0.10.10 and 0.11.4 (both affected)
  • better-auth: 1.4.9

Possibly related

#291 reports custom payload fields missing from the first JWT. The reporter hypothesized trigger/transaction timing, but this async-spread bug is a plausible alternative root cause, worth checking whether their definePayload (or any trigger populating the user doc) is async.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions