onLinkAccount Not Triggered for Anonymous Users #680

Closed
opened 2026-03-13 08:00:02 -05:00 by GiteaMirror · 9 comments
Owner

Originally created by @saturn30 on GitHub (Feb 16, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

The onLinkAccount callback is not triggered despite multiple attempts under the following conditions:

  1. Sign in as an anonymous user.
  2. Call authClient.signIn.social.
  3. Instead of updating the existing anonymous user, a new user row is created for the social account.
  4. The client session is updated to the newly created user.
  5. During this process, the onLinkAccount callback is never executed.

Environment
This issue occurs in an Expo-based app. Below is the current configuration:


Is this the expected behavior? If not, any guidance would be appreciated.

### Current vs. Expected behavior

Current Behavior:
- When an anonymous user attempts to sign in using a social provider (authClient.signIn.social), a new user row is created instead of updating the existing anonymous user.
- The client session is updated to this new user.
- The onLinkAccount callback is never triggered.

Expected Behavior:
- The onLinkAccount callback should be executed when an anonymous user links a social account.
- The existing anonymous user should be updated rather than creating a new user row.
- The client session should be updated to the linked account while retaining the previous user's data.


### What version of Better Auth are you using?

1.1.18

### Provide environment information

```bash
macOS
expo - ios18.2 simulator

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

Backend, Client

Auth config (if applicable)

Server:

betterAuth({
    database: drizzleAdapter(db, {
      provider: "sqlite",
      schema: authSchemas,
    }),
    secret: c.env.BETTER_AUTH_SECRET,
    baseURL: c.env.BETTER_AUTH_URL,
    plugins: [
      anonymous({
        onLinkAccount: async ({ anonymousUser, newUser }) => {
          console.log("run");
        },
      }),
      expo(),
    ],
    trustedOrigins: ["app://"],
    session: {
      expiresIn: 60 * 60 * 24 * 60,
      updateAge: 60 * 60 * 24,
    },
    advanced: {
      cookiePrefix: "app",
    },
    socialProviders: {
      google: {
        clientId: c.env.GOOGLE_CLIENT_ID,
        clientSecret: c.env.GOOGLE_CLIENT_SECRET,
      },
    },
  });

client:

createAuthClient({
  baseURL: "http://localhost:8787",
  plugins: [
    expoClient({
      scheme: "app",
      storagePrefix: "app",
      storage: SecureStore,
    }),
    anonymousClient(),
  ],
});

Additional context

No response

Originally created by @saturn30 on GitHub (Feb 16, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce The onLinkAccount callback is not triggered despite multiple attempts under the following conditions: 1. Sign in as an anonymous user. 2. Call authClient.signIn.social. 3. Instead of updating the existing anonymous user, a new user row is created for the social account. 4. The client session is updated to the newly created user. 5. During this process, the onLinkAccount callback is never executed. Environment This issue occurs in an Expo-based app. Below is the current configuration: ``` Is this the expected behavior? If not, any guidance would be appreciated. ### Current vs. Expected behavior Current Behavior: - When an anonymous user attempts to sign in using a social provider (authClient.signIn.social), a new user row is created instead of updating the existing anonymous user. - The client session is updated to this new user. - The onLinkAccount callback is never triggered. Expected Behavior: - The onLinkAccount callback should be executed when an anonymous user links a social account. - The existing anonymous user should be updated rather than creating a new user row. - The client session should be updated to the linked account while retaining the previous user's data. ### What version of Better Auth are you using? 1.1.18 ### Provide environment information ```bash macOS expo - ios18.2 simulator ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) Server: ```ts betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", schema: authSchemas, }), secret: c.env.BETTER_AUTH_SECRET, baseURL: c.env.BETTER_AUTH_URL, plugins: [ anonymous({ onLinkAccount: async ({ anonymousUser, newUser }) => { console.log("run"); }, }), expo(), ], trustedOrigins: ["app://"], session: { expiresIn: 60 * 60 * 24 * 60, updateAge: 60 * 60 * 24, }, advanced: { cookiePrefix: "app", }, socialProviders: { google: { clientId: c.env.GOOGLE_CLIENT_ID, clientSecret: c.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` client: ```ts createAuthClient({ baseURL: "http://localhost:8787", plugins: [ expoClient({ scheme: "app", storagePrefix: "app", storage: SecureStore, }), anonymousClient(), ], }); ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 08:00:02 -05:00
Author
Owner

@dheerajsarwaiya commented on GitHub (Apr 6, 2025):

This is not working for phone authentication as well. I allow user to link their accounts to email or phone. While email password sign up link account is working, the phone link account is not working. It is not triggering onLinkAccount

@dheerajsarwaiya commented on GitHub (Apr 6, 2025): This is not working for phone authentication as well. I allow user to link their accounts to email or phone. While email password sign up link account is working, the phone link account is not working. It is not triggering onLinkAccount
Author
Owner

@chribjel commented on GitHub (Apr 22, 2025):

This also happens sometimes with magicLink auth.

I believe it is because the anonymous user session is not in the new tab/browser that is opened by the magic link. Thus better-auth thinks it is just a new user, without an existing anon user.

Some info about this should

@chribjel commented on GitHub (Apr 22, 2025): This also happens sometimes with magicLink auth. I believe it is because the anonymous user session is not in the new tab/browser that is opened by the magic link. Thus better-auth thinks it is just a new user, without an existing anon user. Some info about this should
Author
Owner

@flygomel commented on GitHub (May 5, 2025):

same behavior, need fix

@flygomel commented on GitHub (May 5, 2025): same behavior, need fix
Author
Owner

@Kinfe123 commented on GitHub (Jun 8, 2025):

what version of better-auth are you using ? and can you please update it to latest if that is the case.

@Kinfe123 commented on GitHub (Jun 8, 2025): what version of better-auth are you using ? and can you please update it to latest if that is the case.
Author
Owner

@SamJbori commented on GitHub (Jun 10, 2025):

Same thing happening with me, I am using Anonymous and Phone Number plugins, onLinkAccount never run, and the anonymous user isn't getting deleted too

"better-auth": "^1.2.8"

export const auth = betterAuth({
  database: mongodbAdapter(dbUser),
/** Tried with and Without */
  account: {
    accountLinking: {
      enabled: true,
    },
  },
  user: {
    additionalFields: {
      phoneNumber: {
        type: "string",
        required: false,
        unique: true,
        validator: { input: ZPhoneNumber, output: ZPhoneNumber },
      },
      phoneNumberVerified: {
        type: "boolean",
        required: false,
        defaultValue: false,
      },
      hsc: {
        type: "string",
        required: false,
        unique: true,
        sortable: true,
      },
      isAnonymous: { type: "boolean", required: false },
      locale: {
        type: "string",
        required: false,
        input: false,
        validator: { input: ZLanguages, output: ZLanguages },
      },
      stores: {
        type: "string[]",
        required: false,
        input: false,
      },
      tester: { type: "string", required: false },
    },
  },
  plugins: [
    anonymous({
      onLinkAccount: async ({ anonymousUser, newUser }) => {
        // perform actions like moving the cart items from anonymous user to the new user
        console.log("Anonymous User", anonymousUser.user.id);
        console.log("New User", newUser.user.id);
        // await collection("user").deleteOne({
        //   _id: new ObjectId(anonymousUser.user.id),
        // });
      },
    }),
    phoneNumber({
      sendOTP: ({ phoneNumber, code }) => {
        // Implement sending OTP code via SMS
        return sendWAOTP(phoneNumber, code).then((r) => {
          if ([HTTPCodes.H200, HTTPCodes.H201].includes(r)) {
            return;
          } else {
            throw new Error("500");
          }
        });
      },
      expiresIn: 600,
      otpLength: 6,
      phoneNumberValidator: (phoneNumber) => {
        return ZPhoneNumber.safeParse(phoneNumber).success;
      },
      signUpOnVerification: {
        getTempEmail: (phoneNumber) => {
          return `${phoneNumber}@my.hamem.com`;
        },
        //optionally, you can also pass `getTempName` function to generate a temporary name for the user
        getTempName: (phoneNumber) => {
          return phoneNumber; //by default, it will use the phone number as the name
        },
      },
    }),

    customSession(async ({ user, session }) => {
      const stores = (
        await collection("Stores")
          .find({ "users.userId": user.id }, { projection: { hsc: true } })
          .toArray()
      ).map((r) => r.hsc);
      return {
        user: {
          ...user,
          stores,
        },
        session,
      };
    }),
    nextCookies(), // nextCookies Always Last
  ],
});

export const getServerSession = async (headers?: Headers): Promise<Session> =>
  (await auth.api.getSession({
    headers: headers ?? (await nextHeaders()),
  })) as Session;

export type Session = typeof auth.$Infer.Session;

@SamJbori commented on GitHub (Jun 10, 2025): Same thing happening with me, I am using `Anonymous` and `Phone Number` plugins, `onLinkAccount` never run, and the `anonymous `user isn't getting deleted too `"better-auth": "^1.2.8"` ```typescript export const auth = betterAuth({ database: mongodbAdapter(dbUser), /** Tried with and Without */ account: { accountLinking: { enabled: true, }, }, user: { additionalFields: { phoneNumber: { type: "string", required: false, unique: true, validator: { input: ZPhoneNumber, output: ZPhoneNumber }, }, phoneNumberVerified: { type: "boolean", required: false, defaultValue: false, }, hsc: { type: "string", required: false, unique: true, sortable: true, }, isAnonymous: { type: "boolean", required: false }, locale: { type: "string", required: false, input: false, validator: { input: ZLanguages, output: ZLanguages }, }, stores: { type: "string[]", required: false, input: false, }, tester: { type: "string", required: false }, }, }, plugins: [ anonymous({ onLinkAccount: async ({ anonymousUser, newUser }) => { // perform actions like moving the cart items from anonymous user to the new user console.log("Anonymous User", anonymousUser.user.id); console.log("New User", newUser.user.id); // await collection("user").deleteOne({ // _id: new ObjectId(anonymousUser.user.id), // }); }, }), phoneNumber({ sendOTP: ({ phoneNumber, code }) => { // Implement sending OTP code via SMS return sendWAOTP(phoneNumber, code).then((r) => { if ([HTTPCodes.H200, HTTPCodes.H201].includes(r)) { return; } else { throw new Error("500"); } }); }, expiresIn: 600, otpLength: 6, phoneNumberValidator: (phoneNumber) => { return ZPhoneNumber.safeParse(phoneNumber).success; }, signUpOnVerification: { getTempEmail: (phoneNumber) => { return `${phoneNumber}@my.hamem.com`; }, //optionally, you can also pass `getTempName` function to generate a temporary name for the user getTempName: (phoneNumber) => { return phoneNumber; //by default, it will use the phone number as the name }, }, }), customSession(async ({ user, session }) => { const stores = ( await collection("Stores") .find({ "users.userId": user.id }, { projection: { hsc: true } }) .toArray() ).map((r) => r.hsc); return { user: { ...user, stores, }, session, }; }), nextCookies(), // nextCookies Always Last ], }); export const getServerSession = async (headers?: Headers): Promise<Session> => (await auth.api.getSession({ headers: headers ?? (await nextHeaders()), })) as Session; export type Session = typeof auth.$Infer.Session; ```
Author
Owner

@Kinfe123 commented on GitHub (Jun 11, 2025):

have you tried signing in and because there is hook call after every anonymous endpoint call checking if there is an attempt for signing in ?

@Kinfe123 commented on GitHub (Jun 11, 2025): have you tried signing in and because there is hook call after every anonymous endpoint call checking if there is an attempt for signing in ?
Author
Owner

@SamJbori commented on GitHub (Jun 12, 2025):

have you tried signing in and because there is hook call after every anonymous endpoint call checking if there is an attempt for signing in ?

Yes I did, I have a useEffect hook that checks if there is a session and sign anonymously if none found

@SamJbori commented on GitHub (Jun 12, 2025): > have you tried signing in and because there is hook call after every anonymous endpoint call checking if there is an attempt for signing in ? Yes I did, I have a `useEffect` hook that checks if there is a session and sign anonymously if none found
Author
Owner

@Kinfe123 commented on GitHub (Jun 13, 2025):

can you try installing this one -

npm i https://pkg.pr.new/better-auth/better-auth@3007

this should work. let me if it does'nt.

@Kinfe123 commented on GitHub (Jun 13, 2025): can you try installing this one - ``` npm i https://pkg.pr.new/better-auth/better-auth@3007 ``` this should work. let me if it does'nt.
Author
Owner

@SamJbori commented on GitHub (Oct 10, 2025):

@Kinfe123 , I am facing the same issue with better-auth@1.3.27, can someone reopen this issue?

@SamJbori commented on GitHub (Oct 10, 2025): @Kinfe123 , I am facing the same issue with `better-auth@1.3.27`, can someone reopen this issue?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#680