[GH-ISSUE #9129] stripe: getCheckoutSessionParams overrides subscription_data and removes plan freeTrial #28603

Open
opened 2026-04-17 20:02:35 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @mattiacerutti on GitHub (Apr 12, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9129

Is this suited for github?

  • Yes, this is suited for github

Reproduction

  1. Configure Better Auth Stripe plugin with a plan that includes a free trial:
subscription: {
  enabled: true,
  plans: [
    {
      name: "pro",
      priceId: "price_xxx",
      freeTrial: {
        days: 14,
      },
    },
  ],
}
  1. Add getCheckoutSessionParams to customize trial end behavior:
getCheckoutSessionParams: async ({ plan }) => {
  if (plan.name !== "pro") return {};

  return {
    params: {
      payment_method_collection: "if_required",
      subscription_data: {
        trial_settings: {
          end_behavior: {
            missing_payment_method: "cancel",
          },
        },
      },
    },
  };
};
  1. Trigger subscription upgrade from client:
await authClient.subscription.upgrade({
  plan: "pro",
  successUrl: "/dashboard",
  cancelUrl: "/pricing",
});

Current vs. Expected behavior

Current:

  • Stripe Checkout does not show the free trial
  • Adding subscription_data in getCheckoutSessionParams appears to overwrite the internally generated subscription_data from freeTrial

Expected:

  • subscription_data provided in getCheckoutSessionParams should be merged with internally generated values
  • The final Checkout Session should include both:
    • the free trial from plan.freeTrial
    • the custom trial_settings.end_behavior.missing_payment_method
  • Stripe Checkout should still display the free trial

What version of Better Auth are you using?

1.6.2

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:55:08 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6020",
    "release": "25.3.0",
    "cpuCount": 12,
    "cpuModel": "Apple M2 Pro",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.12 GB"
  },
  "node": {
    "version": "v24.13.0",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.6"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.2.1"
    },
    {
      "name": "react",
      "version": "19.2.4"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^7.4.2"
    }
  ],
  "betterAuth": {
    "version": "Unknown",
    "config": null,
    "error": "Converting circular structure to JSON\n    --> starting at object with constructor 'Stripe'\n    |     property 'account' -> object with constructor 'Constructor'\n    --- property '_stripe' closes the circle"
  }
}

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

Backend

Auth config (if applicable)


Additional context

This behavior is not documented. It is unclear whether this is intentional or a bug.

Originally created by @mattiacerutti on GitHub (Apr 12, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9129 ### Is this suited for github? - [x] Yes, this is suited for github ### Reproduction 1. Configure Better Auth Stripe plugin with a plan that includes a free trial: ``` subscription: { enabled: true, plans: [ { name: "pro", priceId: "price_xxx", freeTrial: { days: 14, }, }, ], } ``` 2. Add getCheckoutSessionParams to customize trial end behavior: ``` getCheckoutSessionParams: async ({ plan }) => { if (plan.name !== "pro") return {}; return { params: { payment_method_collection: "if_required", subscription_data: { trial_settings: { end_behavior: { missing_payment_method: "cancel", }, }, }, }, }; }; ``` 3. Trigger subscription upgrade from client: ``` await authClient.subscription.upgrade({ plan: "pro", successUrl: "/dashboard", cancelUrl: "/pricing", }); ``` ### Current vs. Expected behavior ## Current: - Stripe Checkout does not show the free trial - Adding `subscription_data` in `getCheckoutSessionParams` appears to overwrite the internally generated `subscription_data` from `freeTrial` ## Expected: - `subscription_data` provided in `getCheckoutSessionParams` should be merged with internally generated values - The final Checkout Session should include both: - the free trial from `plan.freeTrial` - the custom `trial_settings.end_behavior.missing_payment_method` - Stripe Checkout should still display the free trial ### What version of Better Auth are you using? 1.6.2 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:55:08 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6020", "release": "25.3.0", "cpuCount": 12, "cpuModel": "Apple M2 Pro", "totalMemory": "16.00 GB", "freeMemory": "0.12 GB" }, "node": { "version": "v24.13.0", "env": "development" }, "packageManager": { "name": "bun", "version": "1.3.6" }, "frameworks": [ { "name": "next", "version": "16.2.1" }, { "name": "react", "version": "19.2.4" } ], "databases": [ { "name": "@prisma/client", "version": "^7.4.2" } ], "betterAuth": { "version": "Unknown", "config": null, "error": "Converting circular structure to JSON\n --> starting at object with constructor 'Stripe'\n | property 'account' -> object with constructor 'Constructor'\n --- property '_stripe' closes the circle" } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript ``` ### Additional context This behavior is not documented. It is unclear whether this is intentional or a bug.
GiteaMirror added the payments label 2026-04-17 20:02:35 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Apr 12, 2026):

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

This is a confirmed bug. In routes.ts, the internal subscription_data (which includes trial_period_days from your freeTrial config) is built first, but then ...params?.params is spread after it — completely replacing subscription_data via JavaScript's shallow merge:

subscription_data: {
    ...freeTrial,  // adds trial_period_days: 14
    metadata: subscriptionMetadata.set(...),
},
...params?.params,  // your subscription_data overwrites the one above

The code already protects session-level metadata by repositioning it after the spread [1], but the same pattern is not applied to subscription_data.

Workaround: Until this is fixed, you can manually include trial_period_days in your getCheckoutSessionParams return value:

getCheckoutSessionParams: async ({ plan }) => {
  if (plan.name !== "pro") return {};
  return {
    params: {
      payment_method_collection: "if_required",
      subscription_data: {
        trial_period_days: plan.freeTrial?.days,
        trial_settings: {
          end_behavior: {
            missing_payment_method: "cancel",
          },
        },
      },
    },
  };
},

The proper fix would be to deep-merge subscription_data (similar to how metadata is handled) so internal fields like trial_period_days aren't lost when custom subscription_data is provided.

To reply, just mention @dosu.


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

<!-- gh-comment-id:4231242795 --> @dosubot[bot] commented on GitHub (Apr 12, 2026): <!-- Greeting --> Hi @mattiacerutti! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a confirmed bug. In [`routes.ts`](https://github.com/better-auth/better-auth/blob/ed2c18df3150ab3a024f7d14a5b5fa58719332f2/packages/stripe/src/routes.ts#L1127-L1141), the internal `subscription_data` (which includes `trial_period_days` from your `freeTrial` config) is built first, but then `...params?.params` is spread **after** it — completely replacing `subscription_data` via JavaScript's shallow merge: ```typescript subscription_data: { ...freeTrial, // adds trial_period_days: 14 metadata: subscriptionMetadata.set(...), }, ...params?.params, // your subscription_data overwrites the one above ``` The code already protects session-level `metadata` by repositioning it after the spread [[1]](https://github.com/better-auth/better-auth/blob/ed2c18df3150ab3a024f7d14a5b5fa58719332f2/packages/stripe/src/routes.ts#L1142-L1151), but the same pattern is **not** applied to `subscription_data`. **Workaround**: Until this is fixed, you can manually include `trial_period_days` in your `getCheckoutSessionParams` return value: ```typescript getCheckoutSessionParams: async ({ plan }) => { if (plan.name !== "pro") return {}; return { params: { payment_method_collection: "if_required", subscription_data: { trial_period_days: plan.freeTrial?.days, trial_settings: { end_behavior: { missing_payment_method: "cancel", }, }, }, }, }; }, ``` The proper fix would be to deep-merge `subscription_data` (similar to how `metadata` is handled) so internal fields like `trial_period_days` aren't lost when custom `subscription_data` is provided. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=fe74f35f-aa9a-41f6-85c8-b2c123e6d07c&feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20confirmed%20bug.%20In%20%5B%60routes.ts%60%5D%28https%3A//github.com/better-auth/better-auth/blob/ed2c18df3150ab3a024f7d14a5b5fa58719332f2/packages/stripe/src/routes.ts%23L1127-L1141%29%2C%20the%20internal%20%60subscription_data%60%20%28which%20includes%20%60trial_period_days%60%20from%20your%20%60freeTrial%60%20config%29%20is%20built%20first%2C%20but%20then%20%60...params%3F.params%60%20is%20spread%20%2A%2Aafter%2A%2A%20it%20%E2%80%94%20completely%20replacing%20%60subscription_data%60%20via%20JavaScript%27s%20shallow%20merge%3A%0A%0A%60%60%60typescript%0Asubscription_data%3A%20%7B%0A%20%20%20%20...freeTrial%2C%20%20//%20adds%20trial_period_days%3A%2014%0A%20%20%20%20metadata%3A%20subscriptionMetadata.set%28...%29%2C%0A%7D%2C%0A...params%3F.params%2C%20%20//%20your%20subscription_data%20overwrites%20the%20one%20above%0A%60%60%60%0A%0AThe%20code%20already%20protects%20session-level%20%60metadata%60%20by%20repositioning%20it%20after%20the%20spread%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/ed2c18df3150ab3a024f7d14a5b5fa58719332f2/packages/stripe/src/routes.ts%23L1142-L1151%29%2C%20but%20the%20same%20pattern%20is%20%2A%2Anot%2A%2A%20applied%20to%20%60subscription_data%60.%0A%0A%2A%2AWorkaround%2A%2A%3A%20Until%20this%20is%20fixed%2C%20you%20can%20manually%20include%20%60trial_period_days%60%20in%20your%20%60getCheckoutSessionParams%60%20return%20value%3A%0A%0A%60%60%60typescript%0AgetCheckoutSessionParams%3A%20async%20%28%7B%20plan%20%7D%29%20%3D%3E%20%7B%0A%20%20if%20%28plan.name%20%21%3D%3D%20%22pro%22%29%20return%20%7B%7D%3B%0A%20%20return%20%7B%0A%20%20%20%20params%3A%20%7B%0A%20%20%20%20%20%20payment_method_collection%3A%20%22if_required%22%2C%0A%20%20%20%20%20%20subscription_data%3A%20%7B%0A%20%20%20%20%20%20%20%20trial_period_days%3A%20plan.freeTrial%3F.days%2C%0A%20%20%20%20%20%20%20%20trial_settings%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20end_behavior%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20missing_payment_method%3A%20%22cancel%22%2C%0A%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%7D%2C%0A%60%60%60%0A%0AThe%20proper%20fix%20would%20be%20to%20deep-merge%20%60subscription_data%60%20%28similar%20to%20how%20%60metadata%60%20is%20handled%29%20so%20internal%20fields%20like%20%60trial_period_days%60%20aren%27t%20lost%20when%20custom%20%60subscription_data%60%20is%20provided.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/9129)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28603