[GH-ISSUE #3429] Add metadata to subscription using stripe plugin #9601

Closed
opened 2026-04-13 05:09:04 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @CyrilPERIE on GitHub (Jul 17, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3429

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  • Setup Better auth
  • Setup Stripe plugin

Current vs. Expected behavior

authClient.subscription.upgrade({
...
metadata: {
consumers: 12,23,34
}
}) 

When using this code i can't see where the metadata goes.
I don't receive them in the event with those event hook: onSubscriptionUpdate ``onSubscriptionComplete
They are not set in the subscription

What version of Better Auth are you using?

1.2.8

Provide environment information

- "@better-auth/stripe": "^1.2.8",
- "better-auth": "^1.2.8",

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

Other

Auth config (if applicable)

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

Additional context

No response

Originally created by @CyrilPERIE on GitHub (Jul 17, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3429 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce - Setup Better auth - Setup Stripe plugin ### Current vs. Expected behavior ``` authClient.subscription.upgrade({ ... metadata: { consumers: 12,23,34 } }) ``` When using this code i can't see where the metadata goes. I don't receive them in the event with those event hook: `onSubscriptionUpdate ``onSubscriptionComplete` They are not set in the subscription ### What version of Better Auth are you using? 1.2.8 ### Provide environment information ```bash - "@better-auth/stripe": "^1.2.8", - "better-auth": "^1.2.8", ``` ### Which area(s) are affected? (Select all that apply) Other ### 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:09:04 -05:00
Author
Owner

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

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

The metadata you pass to authClient.subscription.upgrade is only attached to the Stripe customer if a new customer is created, and is not automatically included in the Stripe subscription or Checkout Session metadata. That's why you don't see it in Stripe events or on the subscription object by default.

To ensure your custom metadata appears in Stripe events and on the subscription, you need to use the getCheckoutSessionParams option in your Stripe plugin config. This lets you explicitly add metadata to the Checkout Session, which Stripe will then propagate to the subscription and include in webhook events.

Here’s how you can do it:

import { stripe } from "@better-auth/stripe";

stripe({
  // ...other options
  subscription: {
    // ...other subscription options
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
      // You can access your custom metadata here, e.g. from the session or subscription
      return {
        params: {
          metadata: {
            consumers: "12,23,34", // or dynamically pull from your context
            // ...any other custom fields
          }
        }
      };
    }
  }
});

Now, the metadata will be included in the Stripe Checkout Session and show up in Stripe events and on the subscription object as expected. Note that the Better Auth subscription model itself does not store metadata, so if you need to persist custom data locally, you’ll need to handle that separately in your own logic.

Reference: Stripe plugin docs | Stripe plugin source

To reply, just mention @dosu.


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

<!-- gh-comment-id:3083928940 --> @dosubot[bot] commented on GitHub (Jul 17, 2025): <!-- Greeting --> Hi @CyrilPERIE! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The metadata you pass to `authClient.subscription.upgrade` is only attached to the Stripe customer if a new customer is created, and is not automatically included in the Stripe subscription or Checkout Session metadata. That's why you don't see it in Stripe events or on the subscription object by default. To ensure your custom metadata appears in Stripe events and on the subscription, you need to use the `getCheckoutSessionParams` option in your Stripe plugin config. This lets you explicitly add metadata to the Checkout Session, which Stripe will then propagate to the subscription and include in webhook events. Here’s how you can do it: ```typescript import { stripe } from "@better-auth/stripe"; stripe({ // ...other options subscription: { // ...other subscription options getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => { // You can access your custom metadata here, e.g. from the session or subscription return { params: { metadata: { consumers: "12,23,34", // or dynamically pull from your context // ...any other custom fields } } }; } } }); ``` Now, the metadata will be included in the Stripe Checkout Session and show up in Stripe events and on the subscription object as expected. Note that the Better Auth subscription model itself does not store metadata, so if you need to persist custom data locally, you’ll need to handle that separately in your own logic. Reference: [Stripe plugin docs](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/docs/content/docs/plugins/stripe.mdx) | [Stripe plugin source](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/packages/stripe/src/index.ts) <!-- 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/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/eb61ca9d-59b2-477a-b4c4-b29451cdff9c?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/3429)
Author
Owner

@CyrilPERIE commented on GitHub (Jul 17, 2025):

In our application, a user can pay for other users.
Once someone is tagged as a consumer, they are considered subscribed.

To handle this, we created a view where a user can select other users to subscribe.

We implemented the subscription logic using await authClient.subscription.upgrade, and we passed metadata like this:

metadata: {
consumerIds: "test,test,test,test",
}

We would like to be able to recover those metadatas in the getCheckoutSessionParams

<!-- gh-comment-id:3084001076 --> @CyrilPERIE commented on GitHub (Jul 17, 2025): In our application, a user can pay for other users. Once someone is tagged as a consumer, they are considered subscribed. To handle this, we created a view where a user can select other users to subscribe. We implemented the subscription logic using await authClient.subscription.upgrade, and we passed metadata like this: metadata: { consumerIds: "test,test,test,test", } We would like to be able to recover those metadatas in the getCheckoutSessionParams
Author
Owner

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

The metadata you pass to subscription.upgrade isn't automatically available in getCheckoutSessionParams—it's only used for Stripe customer creation if a new customer is made, and not persisted elsewhere by default. To access your custom metadata (like consumerIds) in getCheckoutSessionParams, you need to explicitly persist it somewhere accessible, such as in the session, a temporary store (like Redis), or a custom database field. Then, in getCheckoutSessionParams, you can retrieve it from that location and include it in the Checkout Session metadata you send to Stripe. This pattern is required because Better Auth does not automatically propagate arbitrary metadata from the upgrade call to downstream hooks or plugin config functions reference.

If you want a concrete example or help implementing this pattern for your stack, let me know!

To reply, just mention @dosu.


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

<!-- gh-comment-id:3084025795 --> @dosubot[bot] commented on GitHub (Jul 17, 2025): <!-- Answer --> The metadata you pass to subscription.upgrade isn't automatically available in getCheckoutSessionParams—it's only used for Stripe customer creation if a new customer is made, and not persisted elsewhere by default. To access your custom metadata (like consumerIds) in getCheckoutSessionParams, you need to explicitly persist it somewhere accessible, such as in the session, a temporary store (like Redis), or a custom database field. Then, in getCheckoutSessionParams, you can retrieve it from that location and include it in the Checkout Session metadata you send to Stripe. This pattern is required because Better Auth does not automatically propagate arbitrary metadata from the upgrade call to downstream hooks or plugin config functions [reference](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/packages/stripe/src/index.ts). If you want a concrete example or help implementing this pattern for your stack, let me know! <!-- 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/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/6ab1661d-9d9a-4e11-af4e-4fd5d7c6ee07?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/3429)
Author
Owner

@CyrilPERIE commented on GitHub (Jul 17, 2025):

I'm developing a web application that allows a parent to register and register children. The parent can also create a subscription for their children. Children are created either by themselves with their email address or via the parent's account (we add a user to the table). A parent needs to be able to subscribe one or more children but also remove a child from a subscription. Prices are also tiered based on the number of children. Should we have multiple subscriptions or one subscription for multiple children? Do you have any implementation ideas? Thank you

<!-- gh-comment-id:3084135681 --> @CyrilPERIE commented on GitHub (Jul 17, 2025): I'm developing a web application that allows a parent to register and register children. The parent can also create a subscription for their children. Children are created either by themselves with their email address or via the parent's account (we add a user to the table). A parent needs to be able to subscribe one or more children but also remove a child from a subscription. Prices are also tiered based on the number of children. Should we have multiple subscriptions or one subscription for multiple children? Do you have any implementation ideas? Thank you
Author
Owner

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

For your use case, the best approach is to use a single subscription with a seat count (quantity) that matches the number of children being subscribed. Better Auth's Stripe plugin supports this model out of the box: the parent is the subscription owner, and the seats field tracks how many children are covered. Stripe handles tiered pricing automatically based on the seat quantity, so you can define your pricing tiers in Stripe and just update the seat count when children are added or removed (docs).

Better Auth does not track which specific users occupy the seats—you'll need to manage the mapping between children and subscription seats in your own database. When a parent adds or removes a child, update your mapping and call authClient.subscription.upgrade with the new seat count. This will update the Stripe subscription and pricing accordingly (plugin code).

To implement this: associate the subscription with the parent (using the referenceId), store which children are assigned to each seat in your app, and enforce that the number of assigned children never exceeds the current seat count. When the parent changes their selection, update both your mapping and the subscription's seat count.

This model is flexible, supports dynamic pricing, and is the recommended pattern for family/group subscriptions with Better Auth and Stripe.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3084144988 --> @dosubot[bot] commented on GitHub (Jul 17, 2025): <!-- Answer --> For your use case, the best approach is to use a single subscription with a seat count (quantity) that matches the number of children being subscribed. Better Auth's Stripe plugin supports this model out of the box: the parent is the subscription owner, and the `seats` field tracks how many children are covered. Stripe handles tiered pricing automatically based on the seat quantity, so you can define your pricing tiers in Stripe and just update the seat count when children are added or removed ([docs](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/docs/content/docs/plugins/stripe.mdx)). Better Auth does not track which specific users occupy the seats—you'll need to manage the mapping between children and subscription seats in your own database. When a parent adds or removes a child, update your mapping and call `authClient.subscription.upgrade` with the new seat count. This will update the Stripe subscription and pricing accordingly ([plugin code](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/packages/stripe/src/index.ts)). To implement this: associate the subscription with the parent (using the `referenceId`), store which children are assigned to each seat in your app, and enforce that the number of assigned children never exceeds the current seat count. When the parent changes their selection, update both your mapping and the subscription's seat count. This model is flexible, supports dynamic pricing, and is the recommended pattern for family/group subscriptions with Better Auth and Stripe. <!-- 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/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/63a9695b-4690-4f1b-baf0-0e48bdf1e1c4?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/3429)
Author
Owner

@CyrilPERIE commented on GitHub (Jul 17, 2025):

Our idea was to create an intermediate table that would store the sponsor, consumerId, status, and subscriptionId which is optional since it's not available before the subscription creation. The questions:
How to put these users in pending status knowing that a person can leave a checkout session?
How to map seats with children?
How to delete a child?
Is it better to create multiple documents in the intermediate table or rather create a document that contains the sponsorId and an array of consumersId?
Thank you for your valuable answers

<!-- gh-comment-id:3084513138 --> @CyrilPERIE commented on GitHub (Jul 17, 2025): Our idea was to create an intermediate table that would store the sponsor, consumerId, status, and subscriptionId which is optional since it's not available before the subscription creation. The questions: How to put these users in pending status knowing that a person can leave a checkout session? How to map seats with children? How to delete a child? Is it better to create multiple documents in the intermediate table or rather create a document that contains the sponsorId and an array of consumersId? Thank you for your valuable answers
Author
Owner

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

To manage pending users, map seats to children, and handle child removal, you'll want to use your intermediate table as the source of truth and synchronize it with subscription events from Stripe/Better Auth.

For pending status: when a parent initiates checkout, create entries in your intermediate table for each selected child with status "pending" and (optionally) a temporary subscriptionId. If the checkout is abandoned, the related Stripe subscription will remain "incomplete"—you can use the Stripe plugin's lifecycle hooks (like onSubscriptionComplete and onSubscriptionUpdate) to detect when a subscription becomes "active" and update your table to set those users to "active" status. Periodically clean up "pending" entries linked to incomplete or expired Stripe subscriptions to avoid orphaned records. Reference

To map seats to children: the Stripe subscription's "seats" (quantity) field should always match the number of "active" children linked to the sponsor in your table. When a parent adds or removes a child, update your mapping and call subscription.upgrade with the new seat count. Enforce in your business logic that the number of active children never exceeds the current seat count. Reference

To delete a child: remove the mapping entry for that child, decrement the seat count, and call subscription.upgrade to update Stripe. If a child is removed before checkout completes, just delete the pending entry. Reference

For your table design: using multiple documents (one per sponsor-consumer pair) is generally more scalable and flexible, especially if you need to query, update, or audit individual relationships or statuses. This approach avoids concurrency issues and makes it easier to manage changes (like removing a single child or updating status). A single document with an array of consumers can work for small, simple use cases, but becomes harder to maintain as your data grows or if you need to handle concurrent updates. Choose the model that best fits your expected usage and database capabilities.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3084522498 --> @dosubot[bot] commented on GitHub (Jul 17, 2025): <!-- Answer --> To manage pending users, map seats to children, and handle child removal, you'll want to use your intermediate table as the source of truth and synchronize it with subscription events from Stripe/Better Auth. For pending status: when a parent initiates checkout, create entries in your intermediate table for each selected child with status "pending" and (optionally) a temporary subscriptionId. If the checkout is abandoned, the related Stripe subscription will remain "incomplete"—you can use the Stripe plugin's lifecycle hooks (like onSubscriptionComplete and onSubscriptionUpdate) to detect when a subscription becomes "active" and update your table to set those users to "active" status. Periodically clean up "pending" entries linked to incomplete or expired Stripe subscriptions to avoid orphaned records. [Reference](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/issues/2929) To map seats to children: the Stripe subscription's "seats" (quantity) field should always match the number of "active" children linked to the sponsor in your table. When a parent adds or removes a child, update your mapping and call subscription.upgrade with the new seat count. Enforce in your business logic that the number of active children never exceeds the current seat count. [Reference](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/packages/stripe/src/index.ts) To delete a child: remove the mapping entry for that child, decrement the seat count, and call subscription.upgrade to update Stripe. If a child is removed before checkout completes, just delete the pending entry. [Reference](https://github.com/better-auth/better-auth/blob/0c4583c015807077cbded89e089f59f7d3d67b48/packages/stripe/src/index.ts) For your table design: using multiple documents (one per sponsor-consumer pair) is generally more scalable and flexible, especially if you need to query, update, or audit individual relationships or statuses. This approach avoids concurrency issues and makes it easier to manage changes (like removing a single child or updating status). A single document with an array of consumers can work for small, simple use cases, but becomes harder to maintain as your data grows or if you need to handle concurrent updates. Choose the model that best fits your expected usage and database capabilities. <!-- 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/0cd44649-d763-45e1-a45e-4039fa41177b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/0cd44649-d763-45e1-a45e-4039fa41177b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/0cd44649-d763-45e1-a45e-4039fa41177b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/0cd44649-d763-45e1-a45e-4039fa41177b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/0cd44649-d763-45e1-a45e-4039fa41177b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/0cd44649-d763-45e1-a45e-4039fa41177b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/0cd44649-d763-45e1-a45e-4039fa41177b?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/3429)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9601