[GH-ISSUE #2440] stripe subscription.upgrade creates a new customer every time #9194

Closed
opened 2026-04-13 04:34:33 -05:00 by GiteaMirror · 8 comments
Owner

Originally created by @DevDuki on GitHub (Apr 25, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2440

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Setup BA with the stripe plugin
  2. Make sure you add createCustomerOnSignUp: true to the stripe plugin config
  3. Locally forward stripe events
  4. Add a button that calls subscription.upgrade()
  5. Notice the customer.created stripe event appearing in the logs

Current vs. Expected behavior

Currently a new customer is created every time subscription.upgrade() is being called.

I expect a subscription to be created on the already existing customer, which was created when a new user is registered.

What version of Better Auth are you using?

1.2.8-beta.2

Provide environment information

- @better-auth/stripe: v1.2.8-beta.2
- stripe: v18.0.0
- OS: MacOS
- Browser: Chromium

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

Other

Auth config (if applicable)

export const auth = betterAuth({
	// Database
	database: prismaAdapter(prisma, {
		provider: 'sqlite'
	}),

	// Email/Password
	emailAndPassword: {
		enabled: true,
		sendResetPassword: async ({ user, url }, request) => {
			// ...
		}
	},
	emailVerification: {
		sendOnSignUp: true,
		autoSignInAfterVerification: true,
		sendVerificationEmail: async ({ user, url }, request) => {
			// ...
		}
	},

	// User
	user: {
		changeEmail: {
			enabled: true,
			sendChangeEmailVerification: async ({ user, url }, request) => {
				// ...
			}
		}
	},

	// Socials
	socialProviders: {
		// ...
	},

	// Plugins
	plugins: [
		passkey(),
		stripe({
			stripeClient: stripeBackendClient,
			stripeWebhookSecret: STRIPE_WEBHOOK_SECRET,
			createCustomerOnSignUp: true,
			subscription: {
				enabled: true,
				plans: [
					// ...
				]
			}
		})
	],

	// Redis
	secondaryStorage: {
		get: async (key) => {
			const value = await redis.get(REDIS_PREFIX + key);
			return value ? value : null;
		},
		set: async (key, value, ttl) => {
			if (ttl) await redis.set(REDIS_PREFIX + key, value, { EX: ttl });
			else await redis.set(REDIS_PREFIX + key, value);
		},
		delete: async (key) => {
			await redis.del(REDIS_PREFIX + key);
		}
	}
});

Additional context

No response

Originally created by @DevDuki on GitHub (Apr 25, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2440 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Setup BA with the stripe plugin 2. Make sure you add `createCustomerOnSignUp: true` to the stripe plugin config 3. Locally forward stripe events 4. Add a button that calls `subscription.upgrade()` 5. Notice the `customer.created` stripe event appearing in the logs ### Current vs. Expected behavior Currently a new customer is created every time `subscription.upgrade()` is being called. I expect a subscription to be created on the already existing customer, which was created when a new user is registered. ### What version of Better Auth are you using? 1.2.8-beta.2 ### Provide environment information ```bash - @better-auth/stripe: v1.2.8-beta.2 - stripe: v18.0.0 - OS: MacOS - Browser: Chromium ``` ### Which area(s) are affected? (Select all that apply) Other ### Auth config (if applicable) ```typescript export const auth = betterAuth({ // Database database: prismaAdapter(prisma, { provider: 'sqlite' }), // Email/Password emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url }, request) => { // ... } }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }, request) => { // ... } }, // User user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, url }, request) => { // ... } } }, // Socials socialProviders: { // ... }, // Plugins plugins: [ passkey(), stripe({ stripeClient: stripeBackendClient, stripeWebhookSecret: STRIPE_WEBHOOK_SECRET, createCustomerOnSignUp: true, subscription: { enabled: true, plans: [ // ... ] } }) ], // Redis secondaryStorage: { get: async (key) => { const value = await redis.get(REDIS_PREFIX + key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await redis.set(REDIS_PREFIX + key, value, { EX: ttl }); else await redis.set(REDIS_PREFIX + key, value); }, delete: async (key) => { await redis.del(REDIS_PREFIX + key); } } }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 04:34:33 -05:00
Author
Owner

@TheYoxy commented on GitHub (Jun 7, 2025):

I ran into this issue, do you provide the subscriptionId params when calling the upgrade ?

<!-- gh-comment-id:2951690650 --> @TheYoxy commented on GitHub (Jun 7, 2025): I ran into this issue, do you provide the `subscriptionId` params when calling the upgrade ?
Author
Owner

@DevDuki commented on GitHub (Jun 7, 2025):

I ran into this issue, do you provide the subscriptionId params when calling the upgrade ?

No I don't. Would that solve anything?

I may have found a reason why this is happening though.

Are you using a secondary storage in your auth config? I noticed that the user in the secondary storage isn't updated the first time the user gets created after registration. The customerId there is still null (the DB is correct though). BA probably checks this and creates a customer conditionally. If I log out and relog in with that same user, the customerId in the secondary storage is set correctly and BA no more creates a new customer when calling the upgrade function. So I think the sync between the secondary storage and the DB is not exactly working as expected when the user is populated with a new customerId. Or are we supposed to handle this ourselves?

<!-- gh-comment-id:2952732811 --> @DevDuki commented on GitHub (Jun 7, 2025): > I ran into this issue, do you provide the `subscriptionId` params when calling the upgrade ? No I don't. Would that solve anything? I may have found a reason why this is happening though. Are you using a secondary storage in your auth config? I noticed that the user in the secondary storage isn't updated the first time the user gets created after registration. The customerId there is still null (the DB is correct though). BA probably checks this and creates a customer conditionally. If I log out and relog in with that same user, the customerId in the secondary storage is set correctly and BA no more creates a new customer when calling the upgrade function. So I think the sync between the secondary storage and the DB is not exactly working as expected when the user is populated with a new customerId. Or are we supposed to handle this ourselves?
Author
Owner

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

I misunderstood the point, but yeah, I think this is related to the secondary storage then. Bc I'm not using the secondary storage for now since I know all the issues that could be caused by cache even if you get better speed while not calling the database every time.

The subscriptionId is only mendatory if you want to upgrade an existing subscription. But it it's created on the first hand, its not possible.

Maybe have a look to the referenceId params, since this will be the id used as reference for the stripe upgrade process.
9fbea84c5f/packages/stripe/src/index.ts (L192-L194)

<!-- gh-comment-id:2953930807 --> @TheYoxy commented on GitHub (Jun 8, 2025): I misunderstood the point, but yeah, I think this is related to the secondary storage then. Bc I'm not using the secondary storage for now since I know all the issues that could be caused by cache even if you get better speed while not calling the database every time. The `subscriptionId` is only mendatory if you want to upgrade an existing subscription. But it it's created on the first hand, its not possible. Maybe have a look to the `referenceId` params, since this will be the id used as reference for the stripe upgrade process. https://github.com/better-auth/better-auth/blob/9fbea84c5f568542088e1e81ffcc43a766bf3f2f/packages/stripe/src/index.ts#L192-L194
Author
Owner

@michalpuchmertl commented on GitHub (Jul 2, 2025):

Do we have any updates on this matter? I'm experiencing the same issue as @DevDuki . I also use secondary storage. I can provide more information if that helps.

<!-- gh-comment-id:3029629351 --> @michalpuchmertl commented on GitHub (Jul 2, 2025): Do we have any updates on this matter? I'm experiencing the same issue as @DevDuki . I also use secondary storage. I can provide more information if that helps.
Author
Owner

@dagmawibabi commented on GitHub (Jul 15, 2025):

@DevDuki does the solution provided by @TheYoxy fix your issue?

<!-- gh-comment-id:3076078748 --> @dagmawibabi commented on GitHub (Jul 15, 2025): @DevDuki does the solution provided by @TheYoxy fix your issue?
Author
Owner

@DevDuki commented on GitHub (Jul 16, 2025):

@dagmawibabi I haven't tested it yet, but even if it did, it looks more like a workaround to me. I think the underlying issue that leads to this bug is the one I described in this https://github.com/better-auth/better-auth/issues/2440#issuecomment-2952732811. If this isn't fixed it could still potentially lead to other issues down the line, which haven't been discovered yet.

I will try yoxy's suggestion when I get to it later today and give a feedback to whether it works or not, though.

<!-- gh-comment-id:3076732033 --> @DevDuki commented on GitHub (Jul 16, 2025): @dagmawibabi I haven't tested it yet, but even if it did, it looks more like a workaround to me. I think the underlying issue that leads to this bug is the one I described in this https://github.com/better-auth/better-auth/issues/2440#issuecomment-2952732811. If this isn't fixed it could still potentially lead to other issues down the line, which haven't been discovered yet. I will try yoxy's suggestion when I get to it later today and give a feedback to whether it works or not, though.
Author
Owner

@DevDuki commented on GitHub (Jul 18, 2025):

@dagmawibabi Sorry I couldn't test it earlier but I found some time now. It still creates a new customer, even when I pass the referenceId

response = await auth.api.upgradeSubscription({
				headers: request.headers,
				body: {
					plan: newPlan,
					successUrl: '/some/path',
					cancelUrl: '/some/path',
					annual: interval === 'year',
					referenceId: locals.user?.id
				},
				asResponse: true
			});

For simplicity I used this as my authorizeReference function in the configs:

authorizeReference: async () => true,

Did I misunderstand something? If so please let me know.

<!-- gh-comment-id:3089590197 --> @DevDuki commented on GitHub (Jul 18, 2025): @dagmawibabi Sorry I couldn't test it earlier but I found some time now. It still creates a new customer, even when I pass the referenceId ```typescript response = await auth.api.upgradeSubscription({ headers: request.headers, body: { plan: newPlan, successUrl: '/some/path', cancelUrl: '/some/path', annual: interval === 'year', referenceId: locals.user?.id }, asResponse: true }); ``` For simplicity I used this as my authorizeReference function in the configs: ```typescript authorizeReference: async () => true, ``` Did I misunderstand something? If so please let me know.
Author
Owner

@DevDuki commented on GitHub (Jul 23, 2025):

Closing this as I can't reproduce this anymore in v1.3.3. Great job team!! 🚀 🥳

<!-- gh-comment-id:3109618671 --> @DevDuki commented on GitHub (Jul 23, 2025): Closing this as I can't reproduce this anymore in `v1.3.3`. Great job team!! 🚀 🥳
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9194