[GH-ISSUE #3545] Stripe plugin incorrectly setting status to active in DB without checking in Stripe #9638

Closed
opened 2026-04-13 05:13:40 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @max-om on GitHub (Jul 22, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3545

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Call authClient.subscription.upgrade from the client, it redirects to Stripe hosted UI for checkout. This creates a stripe customer if does not exist and creates an entry in the DB subscriptions table with status incomplete.
  2. Click on the back button on the Stripe hosted checkout without doing a checkout. You'll be redirected back to the app
  3. Make a call again to authClient.subscription.upgrade from the client, it redirects to Stripe hosted UI for checkout

Now, the status in the DB is changed to active without checking the actual status in Stripe.

Current vs. Expected behavior

Subscription is being set to active without checking the stripe_subscription_id in Stripe. If a row is not active or trialing, its status is set to active if a second checkout session is created

https://github.com/better-auth/better-auth/blob/main/packages/stripe/src/index.ts#L351-L353
https://github.com/better-auth/better-auth/blob/main/packages/stripe/src/index.ts#L399-L413

Expected. whenever a checkout session is created, if an entry exists in the db and stripe_subscription_id is NULL, it should not update the status without checking for subscriptions in stripe

What version of Better Auth are you using?

1.3.2

Provide environment information

- MacOS

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

Package

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @max-om on GitHub (Jul 22, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3545 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Call authClient.subscription.upgrade from the client, it redirects to Stripe hosted UI for checkout. This creates a stripe customer if does not exist and creates an entry in the DB subscriptions table with status incomplete. 2. Click on the back button on the Stripe hosted checkout without doing a checkout. You'll be redirected back to the app 3. Make a call again to authClient.subscription.upgrade from the client, it redirects to Stripe hosted UI for checkout Now, the status in the DB is changed to active without checking the actual status in Stripe. ### Current vs. Expected behavior Subscription is being set to active without checking the stripe_subscription_id in Stripe. If a row is not active or trialing, its status is set to active if a second checkout session is created https://github.com/better-auth/better-auth/blob/main/packages/stripe/src/index.ts#L351-L353 https://github.com/better-auth/better-auth/blob/main/packages/stripe/src/index.ts#L399-L413 Expected. whenever a checkout session is created, if an entry exists in the db and stripe_subscription_id is NULL, it should not update the status without checking for subscriptions in stripe ### What version of Better Auth are you using? 1.3.2 ### Provide environment information ```bash - MacOS ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 05:13:40 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 22, 2025):

You're correct—there's a gap in the current Stripe plugin logic. When a user cancels out of the Stripe Checkout and retries, the code updates the local subscription status to "active" without verifying the actual status in Stripe. This happens because the code checks for an "incomplete" subscription in the DB and sets it to "active" without making an API call to Stripe to confirm the subscription's real status. This can result in the DB showing "active" even if the Stripe subscription was never completed, matching exactly what you described (source).

The expected behavior would be to check with Stripe (using the stripe_subscription_id if present) before updating the local status. If the subscription is still incomplete or not present in Stripe, the DB status should not be set to "active".

A similar issue was discussed in issue #2929 and addressed in PR #2930, but the specific problem of not verifying with Stripe before updating the status remains.

To fix this, you’d want to add a step that queries Stripe for the subscription status before updating the local DB. Only set the status to "active" if Stripe confirms the subscription is actually active or trialing.

Let me know if you want a code snippet or more details on how to implement this check.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3102580472 --> @dosubot[bot] commented on GitHub (Jul 22, 2025): <!-- Answer --> You're correct—there's a gap in the current Stripe plugin logic. When a user cancels out of the Stripe Checkout and retries, the code updates the local subscription status to "active" without verifying the actual status in Stripe. This happens because the code checks for an "incomplete" subscription in the DB and sets it to "active" without making an API call to Stripe to confirm the subscription's real status. This can result in the DB showing "active" even if the Stripe subscription was never completed, matching exactly what you described ([source](https://github.com/better-auth/better-auth/blob/8fbe8f03a552f7af4e69178d0f84844969050601/packages/stripe/src/index.ts)). The expected behavior would be to check with Stripe (using the `stripe_subscription_id` if present) before updating the local status. If the subscription is still incomplete or not present in Stripe, the DB status should not be set to "active". A similar issue was discussed in [issue #2929](https://github.com/better-auth/better-auth/issues/2929) and addressed in [PR #2930](https://github.com/better-auth/better-auth/pull/2930), but the specific problem of not verifying with Stripe before updating the status remains. To fix this, you’d want to add a step that queries Stripe for the subscription status before updating the local DB. Only set the status to "active" if Stripe confirms the subscription is actually active or trialing. Let me know if you want a code snippet or more details on how to implement this check. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/7eb93219-950f-4474-82b5-b07870a900a9?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3545)
Author
Owner

@SimtecConsult commented on GitHub (Jul 22, 2025):

I've noticed the same thing when testing the plugin.

It seems to keep it as Incomplete if you use the link in the Stripe payment page to go back to your application, however, if you use the browser back button then the Subscription is set to Active.

<!-- gh-comment-id:3102641442 --> @SimtecConsult commented on GitHub (Jul 22, 2025): I've noticed the same thing when testing the plugin. It seems to keep it as Incomplete if you use the link in the Stripe payment page to go back to your application, however, if you use the browser back button then the Subscription is set to Active.
Author
Owner

@max-om commented on GitHub (Jul 22, 2025):

@SimtecConsult , for me it happens with both the browser back button and the go back link in the stripe hosted UI. It seems to be related to this code that was pushed: https://github.com/better-auth/better-auth/pull/2930

<!-- gh-comment-id:3102659805 --> @max-om commented on GitHub (Jul 22, 2025): @SimtecConsult , for me it happens with both the browser back button and the go back link in the stripe hosted UI. It seems to be related to this code that was pushed: https://github.com/better-auth/better-auth/pull/2930
Author
Owner

@max-om commented on GitHub (Jul 22, 2025):

@dosu how to implement a check as a temporary fix ?

<!-- gh-comment-id:3102768100 --> @max-om commented on GitHub (Jul 22, 2025): @dosu how to implement a check as a temporary fix ?
Author
Owner

@dosubot[bot] commented on GitHub (Jul 22, 2025):

To implement a temporary fix, add a check in the upgradeSubscription handler (in packages/stripe/src/index.ts) before updating any "incomplete" subscription to "active". Use the Stripe API to fetch the real subscription status and only update the DB if Stripe confirms it's "active" or "trialing"—otherwise, leave it as "incomplete".

Here's a code snippet you can adapt for the relevant section (right before the update that sets status: "active"):

if (incompleteSubscription && incompleteSubscription.stripeSubscriptionId) {
  // Query Stripe for the real status
  const stripeSub = await client.subscriptions.retrieve(incompleteSubscription.stripeSubscriptionId).catch(() => null);

  if (stripeSub && (stripeSub.status === "active" || stripeSub.status === "trialing")) {
    // Only now update the DB to active
    await ctx.context.adapter.update({
      model: "subscription",
      update: {
        ...incompleteSubscription,
        plan: plan.name.toLowerCase(),
        seats: ctx.body.seats || 1,
        stripeCustomerId: customerId,
        status: "active",
      },
      where: [{ field: "id", value: incompleteSubscription.id }],
    });
    subscription = { ...incompleteSubscription, plan: plan.name.toLowerCase(), seats: ctx.body.seats || 1, stripeCustomerId: customerId };
  } else {
    // Otherwise, do not update to active
    // Optionally, log or handle this case
  }
}

Insert this logic where the code currently updates the incomplete subscription to "active" without checking Stripe (see index.ts). This mirrors the secure pattern used in the Stripe webhook handlers, which always sync local status with Stripe's actual status before updating the DB (reference).

Caveat: This is a quick patch. For full reliability, Stripe webhooks should remain the source of truth for subscription status, but this will prevent the most obvious DB/Stripe mismatches in the upgrade flow.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3102785614 --> @dosubot[bot] commented on GitHub (Jul 22, 2025): <!-- Answer --> To implement a temporary fix, add a check in the upgradeSubscription handler (in packages/stripe/src/index.ts) before updating any "incomplete" subscription to "active". Use the Stripe API to fetch the real subscription status and only update the DB if Stripe confirms it's "active" or "trialing"—otherwise, leave it as "incomplete". Here's a code snippet you can adapt for the relevant section (right before the update that sets status: "active"): ```typescript if (incompleteSubscription && incompleteSubscription.stripeSubscriptionId) { // Query Stripe for the real status const stripeSub = await client.subscriptions.retrieve(incompleteSubscription.stripeSubscriptionId).catch(() => null); if (stripeSub && (stripeSub.status === "active" || stripeSub.status === "trialing")) { // Only now update the DB to active await ctx.context.adapter.update({ model: "subscription", update: { ...incompleteSubscription, plan: plan.name.toLowerCase(), seats: ctx.body.seats || 1, stripeCustomerId: customerId, status: "active", }, where: [{ field: "id", value: incompleteSubscription.id }], }); subscription = { ...incompleteSubscription, plan: plan.name.toLowerCase(), seats: ctx.body.seats || 1, stripeCustomerId: customerId }; } else { // Otherwise, do not update to active // Optionally, log or handle this case } } ``` Insert this logic where the code currently updates the incomplete subscription to "active" without checking Stripe (see [index.ts](https://github.com/better-auth/better-auth/blob/main/packages/stripe/src/index.ts#L351-L353)). This mirrors the secure pattern used in the Stripe webhook handlers, which always sync local status with Stripe's actual status before updating the DB ([reference](https://github.com/better-auth/better-auth/blob/8fbe8f03a552f7af4e69178d0f84844969050601/packages/stripe/src/hooks.ts)). Caveat: This is a quick patch. For full reliability, Stripe webhooks should remain the source of truth for subscription status, but this will prevent the most obvious DB/Stripe mismatches in the upgrade flow. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/b7c1150a-68cc-4ce7-b8a1-75c2ef8d03f6?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3545)
Author
Owner

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

@Bekacru Is it worth re-opening this with this comment? https://github.com/better-auth/better-auth/pull/3554#issuecomment-3106051679

<!-- gh-comment-id:3106346325 --> @nowy commented on GitHub (Jul 23, 2025): @Bekacru Is it worth re-opening this with this comment? https://github.com/better-auth/better-auth/pull/3554#issuecomment-3106051679
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9638