Anonymous not working with Convex #2270

Closed
opened 2026-03-13 09:39:58 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @rodrigosdev on GitHub (Nov 7, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Reproduction Steps

  1. Enable the anonymous plugin:
betterAuth({
  plugins: [
    anonymous({
      emailDomainName: 'localhost:3000'
    })
  ]
})
  1. Call anonymous sign-in from client:
const result = await authClient.signIn.anonymous()
  1. Check Convex logs - you'll see the deletion happening

Current vs. Expected behavior

Summary

The Better Auth anonymous plugin has a critical bug where it deletes newly created anonymous users immediately after creation when used with the Convex adapter. This makes anonymous authentication completely non-functional.

Expected Behavior

  1. "Login Anonymously"
  2. Better Auth creates a new anonymous user with email like temp-xyz@localhost:3000
  3. Session is created and stored
  4. User remains authenticated and can use the app

Actual Behavior

  1. "Login Anonymously"
  2. Better Auth creates a new anonymous user
  3. Better Auth creates a session
  4. Better Auth immediately deletes the user
  5. Session lookup fails because user was deleted
  6. User appears unauthenticated

Root Cause Analysis

The anonymous plugin has built-in cleanup logic that:

  1. Checks if the incoming request has an existing anonymous JWT cookie
  2. If yes, deletes the old anonymous user to prevent accumulation
  3. Creates a new anonymous user

The bug: This cleanup logic runs DURING the sign-in request, not before. The sequence is:

1. Create new user (id: m5767mek...)
2. Create new session (token: hI4IfkTS...)
3. Generate JWT and prepare to send it
4. Detect "old" JWT in request cookies (from milliseconds ago!)
5. Delete the user that was JUST created
6. Send response with JWT for deleted user
7. Browser receives JWT but user is gone

What version of Better Auth are you using?

1.3.27

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103",
    "release": "24.6.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.82 GB"
  },
  "node": {
    "version": "v22.19.0",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.1"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.0.1"
    },
    {
      "name": "react",
      "version": "19.2.0"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "1.3.27",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { betterAuth } from "better-auth";
import { anonymous } from "better-auth/plugins";
import { v } from "convex/values";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";

const siteUrl = process.env.SITE_URL
  ? process.env.SITE_URL
  : "http://localhost:3000";

// The component client has methods needed for integrating Convex with Better Auth,
// as well as helper methods for general use.
export const authComponent = createClient<DataModel>(components.betterAuth);

export const createAuth = (
  ctx: GenericCtx<DataModel>,
  { optionsOnly } = { optionsOnly: false }
) => {
  return betterAuth({
    // disable logging when createAuth is called just to generate options.
    // this is not required, but there's a lot of noise in logs without it.
    logger: {
      disabled: optionsOnly,
    },
    baseURL: siteUrl,
    database: authComponent.adapter(ctx),
    // Configure simple, non-verified email/password to get started
    emailAndPassword: {
      enabled: true,
      requireEmailVerification: false,
    },
    plugins: [
      // The Convex plugin is required for Convex compatibility
      convex(),
      anonymous({
        disableDeleteAnonymousUser: true,
        onLinkAccount: async ({ anonymousUser, newUser }) => {
          // Need to delete the anonymous user after linking
          // There is a bug with the anonymous user
          // biome-ignore lint/suspicious/noConsole: Temporary
          await console.log({ anonymousUser, newUser });
        },
      }),
    ],
  });
};

// Example function for getting the current user
// Feel free to edit, omit, etc.
export const getCurrentUser = query({
  args: {},
  returns: v.union(v.any(), v.null()),
  handler: async (ctx) => authComponent.getAuthUser(ctx),
});


// This is the contents of the `auth.ts` inside the convex folder

Additional context

Attempted Workarounds (All Failed)

Workaround 1: Sign Out Before Sign In

// Check for existing JWT and sign out first
if (document.cookie.includes('better-auth.convex_jwt=')) {
  await authClient.signOut()
  await new Promise(resolve => setTimeout(resolve, 800))
}
await authClient.signIn.anonymous()

Result: Failed - The old JWT is still in the request headers because:

  • HTTP requests are sent with cookies at request-time
  • Even if we clear cookies client-side, the in-flight request already has them
  • The server sees the old JWT and triggers cleanup
Originally created by @rodrigosdev on GitHub (Nov 7, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ## Reproduction Steps 1. Enable the anonymous plugin: ```ts betterAuth({ plugins: [ anonymous({ emailDomainName: 'localhost:3000' }) ] }) ``` 2. Call anonymous sign-in from client: ```ts const result = await authClient.signIn.anonymous() ``` 3. Check Convex logs - you'll see the deletion happening ### Current vs. Expected behavior ## Summary The Better Auth anonymous plugin has a critical bug where it deletes newly created anonymous users immediately after creation when used with the Convex adapter. This makes anonymous authentication completely non-functional. ## Expected Behavior 1. "Login Anonymously" 2. Better Auth creates a new anonymous user with email like temp-xyz@localhost:3000 3. Session is created and stored 4. User remains authenticated and can use the app ## Actual Behavior 1. "Login Anonymously" 2. Better Auth creates a new anonymous user ✅ 3. Better Auth creates a session ✅ 4. Better Auth immediately deletes the user ❌ 5. Session lookup fails because user was deleted ❌ 6. User appears unauthenticated ❌ ## Root Cause Analysis The anonymous plugin has built-in cleanup logic that: 1. Checks if the incoming request has an existing anonymous JWT cookie 2. If yes, deletes the old anonymous user to prevent accumulation 3. Creates a new anonymous user **The bug**: This cleanup logic runs **DURING** the sign-in request, not before. The sequence is: ```bash 1. Create new user (id: m5767mek...) 2. Create new session (token: hI4IfkTS...) 3. Generate JWT and prepare to send it 4. Detect "old" JWT in request cookies (from milliseconds ago!) 5. Delete the user that was JUST created 6. Send response with JWT for deleted user 7. Browser receives JWT but user is gone ``` ### What version of Better Auth are you using? 1.3.27 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103", "release": "24.6.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "16.00 GB", "freeMemory": "0.82 GB" }, "node": { "version": "v22.19.0", "env": "development" }, "packageManager": { "name": "bun", "version": "1.3.1" }, "frameworks": [ { "name": "next", "version": "16.0.1" }, { "name": "react", "version": "19.2.0" } ], "databases": null, "betterAuth": { "version": "1.3.27", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { createClient, type GenericCtx } from "@convex-dev/better-auth"; import { convex } from "@convex-dev/better-auth/plugins"; import { betterAuth } from "better-auth"; import { anonymous } from "better-auth/plugins"; import { v } from "convex/values"; import { components } from "./_generated/api"; import type { DataModel } from "./_generated/dataModel"; import { query } from "./_generated/server"; const siteUrl = process.env.SITE_URL ? process.env.SITE_URL : "http://localhost:3000"; // The component client has methods needed for integrating Convex with Better Auth, // as well as helper methods for general use. export const authComponent = createClient<DataModel>(components.betterAuth); export const createAuth = ( ctx: GenericCtx<DataModel>, { optionsOnly } = { optionsOnly: false } ) => { return betterAuth({ // disable logging when createAuth is called just to generate options. // this is not required, but there's a lot of noise in logs without it. logger: { disabled: optionsOnly, }, baseURL: siteUrl, database: authComponent.adapter(ctx), // Configure simple, non-verified email/password to get started emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [ // The Convex plugin is required for Convex compatibility convex(), anonymous({ disableDeleteAnonymousUser: true, onLinkAccount: async ({ anonymousUser, newUser }) => { // Need to delete the anonymous user after linking // There is a bug with the anonymous user // biome-ignore lint/suspicious/noConsole: Temporary await console.log({ anonymousUser, newUser }); }, }), ], }); }; // Example function for getting the current user // Feel free to edit, omit, etc. export const getCurrentUser = query({ args: {}, returns: v.union(v.any(), v.null()), handler: async (ctx) => authComponent.getAuthUser(ctx), }); // This is the contents of the `auth.ts` inside the convex folder ``` ### Additional context ## Attempted Workarounds (All Failed) ### Workaround 1: Sign Out Before Sign In ```ts // Check for existing JWT and sign out first if (document.cookie.includes('better-auth.convex_jwt=')) { await authClient.signOut() await new Promise(resolve => setTimeout(resolve, 800)) } await authClient.signIn.anonymous() ``` **Result**: Failed - The old JWT is still in the request headers because: - HTTP requests are sent with cookies at request-time - Even if we clear cookies client-side, the in-flight request already has them - The server sees the old JWT and triggers cleanup
GiteaMirror added the bug label 2026-03-13 09:39:58 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 7, 2025):

Thanks for raising this issue. It sounds like the anonymous plugin’s cleanup logic is mistakenly deleting the freshly created anonymous user during the sign‐in process when used with the Convex adapter. In your reproduction steps, the process creates the new user and session successfully but then immediately deletes the user because an “old” JWT is detected in the cookies—even though you have disabled automatic deletion. This behavior indicates a race condition or mis-sequencing of cleanup logic.

A few troubleshooting steps to consider:
• Confirm that no stray cookies (or earlier anonymous tokens) are being sent with the request that might trigger deletion.
• If possible, try clearing your cookies completely before the anonymous sign-in request.
• As a workaround, some have temporarily reverted to an earlier Better Auth version where this issue didn’t occur.
• Finally, please open a GitHub issue with detailed reproduction steps so our team can investigate and patch the cleanup logic timing.

Does this match what you’ve observed, or is there anything else you’d like to share? Tag @better-auth-agent if you would like an answer from the Agent.

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 7, 2025): Thanks for raising this issue. It sounds like the anonymous plugin’s cleanup logic is mistakenly deleting the freshly created anonymous user during the sign‐in process when used with the Convex adapter. In your reproduction steps, the process creates the new user and session successfully but then immediately deletes the user because an “old” JWT is detected in the cookies—even though you have disabled automatic deletion. This behavior indicates a race condition or mis-sequencing of cleanup logic. A few troubleshooting steps to consider: • Confirm that no stray cookies (or earlier anonymous tokens) are being sent with the request that might trigger deletion. • If possible, try clearing your cookies completely before the anonymous sign-in request. • As a workaround, some have temporarily reverted to an earlier Better Auth version where this issue didn’t occur. • Finally, please open a GitHub issue with detailed reproduction steps so our team can investigate and patch the cleanup logic timing. Does this match what you’ve observed, or is there anything else you’d like to share? Tag @better-auth-agent if you would like an answer from the Agent. <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2270