[GH-ISSUE #3978] Stripe plugin returns stale data when listing active subscriptions #27105

Closed
opened 2026-04-17 17:55:20 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @m90 on GitHub (Aug 13, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3978

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Using the Stripe plugin at version 1.3.6 and the stripe package at 18.4.0, running all code on the server

  1. Create a new subscription using upgradeSubscription
  2. In Stripe, cancel the subscription immediately
  3. Request all active subscriptions using listActiveSubscriptions
  4. See a subscription that still has status: active returned
  5. Using the same stripe cllient that is being passed to better auth, look for active subscriptions, receive an empty response
  6. Using the same stripe cllient that is being passed to better auth, look for canceled subscriptions, receive the same subscription that better auth returns as active, just with status: canceled

Current vs. Expected behavior

I would expect the data returned from better auth to be in sync with what Stripe returns.

Running this code

const fromStripe = await stripeClient.subscriptions.list({
  status: "canceled",
});
console.log("FROM STRIPE", fromStripe.data);

const fromApi = await auth.api.listActiveSubscriptions({
  query: {
    referenceId: ctx.session.session?.activeOrganizationId,
  },
  headers: ctx.headers,
});

console.log("FROM API", fromApi);

I receive

FROM STRIPE [
  {
    id: 'sub_1RvfEpBNmaMMVvib8vfY3VWS',
    object: 'subscription',
    application: null,
    application_fee_percent: null,
    automatic_tax: { disabled_reason: null, enabled: false, liability: null },
    billing_cycle_anchor: 1755093670,
    billing_cycle_anchor_config: null,
    billing_mode: { type: 'classic' },
    billing_thresholds: null,
    cancel_at: null,
    cancel_at_period_end: false,
    canceled_at: 1755093850,
    cancellation_details: { comment: null, feedback: null, reason: 'cancellation_requested' },
    collection_method: 'charge_automatically',
    created: 1755093670,
    currency: 'usd',
    customer: 'CUSTOMERIDREDACTED',
    days_until_due: null,
    default_payment_method: 'pm_1RvfEnBNmaMMVvibPLTekdc3',
    default_source: null,
    default_tax_rates: [],
    description: null,
    discounts: [],
    ended_at: 1755093850,
    invoice_settings: { account_tax_ids: null, issuer: [Object] },
    items: {
      object: 'list',
      data: [Array],
      has_more: false,
      total_count: 1,
      url: '/v1/subscription_items?subscription=sub_1RvfEpBNmaMMVvib8vfY3VWS'
    },
    latest_invoice: 'in_1RvfEoBNmaMMVvibtI5Z9Sk0',
    livemode: false,
    metadata: {},
    next_pending_invoice_item_invoice: null,
    on_behalf_of: null,
    pause_collection: null,
    payment_settings: {
      payment_method_options: [Object],
      payment_method_types: null,
      save_default_payment_method: 'off'
    },
    pending_invoice_item_interval: null,
    pending_setup_intent: null,
    pending_update: null,
    plan: {
      id: 'price_1RvKhLBNmaMMVvibJ0JrUlGf',
      object: 'plan',
      active: true,
      amount: 4995,
      amount_decimal: '4995',
      billing_scheme: 'per_unit',
      created: 1755014715,
      currency: 'usd',
      interval: 'month',
      interval_count: 1,
      livemode: false,
      metadata: {},
      meter: null,
      nickname: null,
      product: 'prod_Sr2mq3WPZQk457',
      tiers_mode: null,
      transform_usage: null,
      trial_period_days: null,
      usage_type: 'licensed'
    },
    quantity: 1,
    schedule: null,
    start_date: 1755093670,
    status: 'canceled',
    test_clock: null,
    transfer_data: null,
    trial_end: null,
    trial_settings: { end_behavior: [Object] },
    trial_start: null
  }
]

FROM BETTER AUTH [
  {
    plan: 'SOME PLAN NAME',
    referenceId: 'cme9o3v790000w7j4peds4m1t',
    stripeCustomerId: 'CUSTOMERIDREDACTED',
    stripeSubscriptionId: 'sub_1RvfEpBNmaMMVvib8vfY3VWS',
    status: 'active',
    periodStart: 2025-08-13T14:01:10.000Z,
    periodEnd: 2025-09-13T14:01:10.000Z,
    cancelAtPeriodEnd: null,
    seats: 1,
    id: 'UnfAQNj0LavVMHAVesUhqX6BwCmmK8zG',
    limits: undefined,
    priceId: 'price_1RvKhLBNmaMMVvibJ0JrUlGf'
  }
]

Note how both subscriptions are of id sub_1RvfEpBNmaMMVvib8vfY3VWS, but have a different status. This persists across application restarts and reboots, so I would be surprised if this is any caching.

What version of Better Auth are you using?

1.3.6

System info

Running:
- Fedora Linux 42
- Node 22.18.0
- Chrome 139.0.7258.66

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

Backend

Auth config (if applicable)

// this is the stripe plugin config only

    stripePlugin({
      schema: {
        subscription: {
          modelName: "StripeSubscription",
        },
      },
      stripeClient,
      stripeWebhookSecret: env.STRIPE_WEBHOOK_SIGNING_SECRET,
      createCustomerOnSignUp: false,
      subscription: {
        enabled: true,
        plans: async function () {
          const products = await getAllProducts();
          return products.map(item => {
            const price = item.default_price as { id: string };
            return {
              name: item.name,
              priceId: price.id,
            } as StripePlan;
          });
        },
        authorizeReference: async ({ user, referenceId }, ctx) => {
          try {
            await db.member.findFirstOrThrow({
              where: {
                userId: user.id,
                organizationId: referenceId
              }
            });
            return true;
          } catch (err) {
            if (err instanceof PrismaClientKnownRequestError && err.code === "P2025") {
              return false;
            }
            throw err;
          }
        },
      },
    }),

// stripe client instantiated like

export const stripeClient = new Stripe(env.STRIPE_SECRET_KEY, {
  apiVersion: "2025-07-30.basil",
});

Additional context

No response

Originally created by @m90 on GitHub (Aug 13, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3978 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Using the Stripe plugin at version 1.3.6 and the stripe package at 18.4.0, running all code on the server 1. Create a new subscription using `upgradeSubscription` 2. In Stripe, cancel the subscription immediately 3. Request all active subscriptions using `listActiveSubscriptions` 4. See a subscription that still has `status: active` returned 5. Using the same stripe cllient that is being passed to better auth, look for active subscriptions, receive an empty response 6. Using the same stripe cllient that is being passed to better auth, look for canceled subscriptions, receive the same subscription that better auth returns as active, just with `status: canceled` ### Current vs. Expected behavior I would expect the data returned from better auth to be in sync with what Stripe returns. Running this code ```js const fromStripe = await stripeClient.subscriptions.list({ status: "canceled", }); console.log("FROM STRIPE", fromStripe.data); const fromApi = await auth.api.listActiveSubscriptions({ query: { referenceId: ctx.session.session?.activeOrganizationId, }, headers: ctx.headers, }); console.log("FROM API", fromApi); ``` I receive ``` FROM STRIPE [ { id: 'sub_1RvfEpBNmaMMVvib8vfY3VWS', object: 'subscription', application: null, application_fee_percent: null, automatic_tax: { disabled_reason: null, enabled: false, liability: null }, billing_cycle_anchor: 1755093670, billing_cycle_anchor_config: null, billing_mode: { type: 'classic' }, billing_thresholds: null, cancel_at: null, cancel_at_period_end: false, canceled_at: 1755093850, cancellation_details: { comment: null, feedback: null, reason: 'cancellation_requested' }, collection_method: 'charge_automatically', created: 1755093670, currency: 'usd', customer: 'CUSTOMERIDREDACTED', days_until_due: null, default_payment_method: 'pm_1RvfEnBNmaMMVvibPLTekdc3', default_source: null, default_tax_rates: [], description: null, discounts: [], ended_at: 1755093850, invoice_settings: { account_tax_ids: null, issuer: [Object] }, items: { object: 'list', data: [Array], has_more: false, total_count: 1, url: '/v1/subscription_items?subscription=sub_1RvfEpBNmaMMVvib8vfY3VWS' }, latest_invoice: 'in_1RvfEoBNmaMMVvibtI5Z9Sk0', livemode: false, metadata: {}, next_pending_invoice_item_invoice: null, on_behalf_of: null, pause_collection: null, payment_settings: { payment_method_options: [Object], payment_method_types: null, save_default_payment_method: 'off' }, pending_invoice_item_interval: null, pending_setup_intent: null, pending_update: null, plan: { id: 'price_1RvKhLBNmaMMVvibJ0JrUlGf', object: 'plan', active: true, amount: 4995, amount_decimal: '4995', billing_scheme: 'per_unit', created: 1755014715, currency: 'usd', interval: 'month', interval_count: 1, livemode: false, metadata: {}, meter: null, nickname: null, product: 'prod_Sr2mq3WPZQk457', tiers_mode: null, transform_usage: null, trial_period_days: null, usage_type: 'licensed' }, quantity: 1, schedule: null, start_date: 1755093670, status: 'canceled', test_clock: null, transfer_data: null, trial_end: null, trial_settings: { end_behavior: [Object] }, trial_start: null } ] FROM BETTER AUTH [ { plan: 'SOME PLAN NAME', referenceId: 'cme9o3v790000w7j4peds4m1t', stripeCustomerId: 'CUSTOMERIDREDACTED', stripeSubscriptionId: 'sub_1RvfEpBNmaMMVvib8vfY3VWS', status: 'active', periodStart: 2025-08-13T14:01:10.000Z, periodEnd: 2025-09-13T14:01:10.000Z, cancelAtPeriodEnd: null, seats: 1, id: 'UnfAQNj0LavVMHAVesUhqX6BwCmmK8zG', limits: undefined, priceId: 'price_1RvKhLBNmaMMVvibJ0JrUlGf' } ] ``` Note how both subscriptions are of id `sub_1RvfEpBNmaMMVvib8vfY3VWS`, but have a different status. This persists across application restarts and reboots, so I would be surprised if this is any caching. ### What version of Better Auth are you using? 1.3.6 ### System info ```bash Running: - Fedora Linux 42 - Node 22.18.0 - Chrome 139.0.7258.66 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript // this is the stripe plugin config only stripePlugin({ schema: { subscription: { modelName: "StripeSubscription", }, }, stripeClient, stripeWebhookSecret: env.STRIPE_WEBHOOK_SIGNING_SECRET, createCustomerOnSignUp: false, subscription: { enabled: true, plans: async function () { const products = await getAllProducts(); return products.map(item => { const price = item.default_price as { id: string }; return { name: item.name, priceId: price.id, } as StripePlan; }); }, authorizeReference: async ({ user, referenceId }, ctx) => { try { await db.member.findFirstOrThrow({ where: { userId: user.id, organizationId: referenceId } }); return true; } catch (err) { if (err instanceof PrismaClientKnownRequestError && err.code === "P2025") { return false; } throw err; } }, }, }), // stripe client instantiated like export const stripeClient = new Stripe(env.STRIPE_SECRET_KEY, { apiVersion: "2025-07-30.basil", }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-17 17:55:20 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 13, 2025):

Hi @m90! I'm Dosu and I’m helping the better-auth team.

The stale subscription status is happening because Better Auth's listActiveSubscriptions endpoint reads from its local database, not directly from Stripe. The local subscription status only updates when Stripe webhook events (like customer.subscription.updated or customer.subscription.deleted) are received and processed by your Better Auth server. If those webhooks aren't delivered or processed correctly, the local data will be out of sync with Stripe—even after restarts, since it's not just a cache issue but a persistent database state. You can see this in the Stripe plugin code.

To resolve this, check your Stripe webhook delivery logs and Better Auth server logs to confirm those events are being received and handled. If you need to force a sync, you could manually update the local subscription status using the Stripe API response, or implement a custom sync endpoint. There’s also a new onEvent callback option in the Stripe plugin that lets you run custom logic after a Stripe event is received, which could help keep things in sync (PR #3622).

If your webhooks are working and you’re still seeing stale data, you may want to update to the latest plugin version or consider adding a feature request for real-time sync.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3184417156 --> @dosubot[bot] commented on GitHub (Aug 13, 2025): <!-- Greeting --> Hi @m90! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The stale subscription status is happening because Better Auth's `listActiveSubscriptions` endpoint reads from its local database, not directly from Stripe. The local subscription status only updates when Stripe webhook events (like `customer.subscription.updated` or `customer.subscription.deleted`) are received and processed by your Better Auth server. If those webhooks aren't delivered or processed correctly, the local data will be out of sync with Stripe—even after restarts, since it's not just a cache issue but a persistent database state. You can see this in the [Stripe plugin code](https://github.com/better-auth/better-auth/blob/c7a272f26137bb1eeafae267d460fc105f4ad714/packages/stripe/src/index.ts). To resolve this, check your Stripe webhook delivery logs and Better Auth server logs to confirm those events are being received and handled. If you need to force a sync, you could manually update the local subscription status using the Stripe API response, or implement a custom sync endpoint. There’s also a new `onEvent` callback option in the Stripe plugin that lets you run custom logic after a Stripe event is received, which could help keep things in sync ([PR #3622](https://github.com/better-auth/better-auth/pull/3622)). If your webhooks are working and you’re still seeing stale data, you may want to update to the latest plugin version or consider adding a feature request for real-time sync. <!-- 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/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/426dd331-d83d-4428-9ce1-736a500ee56b?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3978)
Author
Owner

@m90 commented on GitHub (Aug 13, 2025):

This indeed fixes the issue.

<!-- gh-comment-id:3184484328 --> @m90 commented on GitHub (Aug 13, 2025): This indeed fixes the 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#27105