[GH-ISSUE #4957] Better Auth Stripe: Subscription Updates Not Persisting to Database #27445

Closed
opened 2026-04-17 18:28:16 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @BABYB0I on GitHub (Sep 28, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4957

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a first subscription (e.g., Gold plan)
  2. Try to update the subscription to a higher plan by passing the subscriptionId to the upgrade method
  3. Check the database - the subscription table still shows the old plan

Current vs. Expected behavior

Current behavior:

  • Stripe creates a new subscription successfully
  • Database subscription table is not updated with the new plan
  • User remains on the old plan in the database

Expected behavior:

  • Stripe updates the existing subscription
  • Database subscription table is updated with the new plan
  • User's subscription reflects the new plan

What version of Better Auth are you using?

1.3.9

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Home",
    "release": "10.0.26100",
    "cpuCount": 12,
    "cpuModel": "13th Gen Intel(R) Core(TM) i7-1355U",
    "totalMemory": "15.69 GB",
    "freeMemory": "0.92 GB"
  },
  "node": {
    "version": "v21.7.1",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.2.21"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.5.2"
    },
    {
      "name": "react",
      "version": "19.1.0"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^6.15.0"
    }
  ],
  "betterAuth": {
    "version": "^1.3.9",
    "config": {
      "emailAndPassword": {
        "enabled": true,
        "autoSignIn": false,
        "password": {},
        "requireEmailVerification": true
      },
      "emailVerification": {
        "sendOnSignUp": true,
        "autoSignInAfterVerification": true
      },
      "socialProviders": {
        "google": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        },
        "discord": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]"
        }
      },
      "hooks": {},
      "databaseHooks": {
        "user": {
          "create": {}
        }
      },
      "user": {
        "additionalFields": {
          "role": {
            "type": [
              "NONE",
              "PRODUCER",
              "ARTIST",
              "ADMIN",
              "OWNER"
            ],
            "input": false
          }
        }
      },
      "session": {
        "expiresIn": 2592000
      },
      "account": {
        "accountLinking": {
          "enabled": false
        }
      },
      "advanced": {
        "database": {
          "generateId": false
        }
      },
      "plugins": [
        {
          "name": "username",
          "config": {
            "id": "username",
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "username": {
                    "type": "string",
                    "required": false,
                    "sortable": true,
                    "unique": true,
                    "returned": true,
                    "transform": {}
                  },
                  "displayUsername": {
                    "type": "string",
                    "required": false,
                    "transform": {}
                  }
                }
              }
            },
            "hooks": {
              "before": [
                {},
                {}
              ]
            },
            "$ERROR_CODES": {
              "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]",
              "EMAIL_NOT_VERIFIED": "Email not verified",
              "UNEXPECTED_ERROR": "Unexpected error",
              "USERNAME_IS_ALREADY_TAKEN": "Username is already taken. Please try another.",
              "USERNAME_TOO_SHORT": "Username is too short",
              "USERNAME_TOO_LONG": "Username is too long",
              "INVALID_USERNAME": "Username is invalid",
              "INVALID_DISPLAY_USERNAME": "Display username is invalid"
            }
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",     
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", 
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "defaultRole": "NONE",
              "adminRoles": [
                "ADMIN",
                "OWNER"
              ],
              "ac": {
                "statements": {
                  "user": [
                    "create",
                    "list",
                    "set-role",
                    "ban",
                    "impersonate",
                    "delete",
                    "set-password",
                    "get",
                    "update"
                  ],
                  "session": [
                    "list",
                    "revoke",
                    "delete"
                  ],
                  "posts": [
                    "create",
                    "read",
                    "update",
                    "delete",
                    "update:own",
                    "delete:own"
                  ]
                }
              },
              "roles": {
                "NONE": {
                  "statements": {
                    "posts": [],
                    "user": [
                      "set-role",
                      "update"
                    ]
                  }
                },
                "PRODUCER": {
                  "statements": {
                    "posts": [
                      "create",
                      "read",
                      "update:own",
                      "delete:own"
                    ],
                    "user": [
                      "set-role"
                    ]
                  }
                },
                "ARTIST": {
                  "statements": {
                    "posts": [
                      "create",
                      "read",
                      "update:own",
                      "delete:own"
                    ],
                    "user": [
                      "set-role"
                    ]
                  }
                },
                "ADMIN": {
                  "statements": {
                    "user": [
                      "update",
                      "delete",
                      "list",
                      "set-role"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ],
                    "posts": [
                      "create",
                      "read",
                      "update",
                      "delete",
                      "update:own",
                      "delete:own"
                    ]
                  }
                },
                "OWNER": {
                  "statements": {
                    "user": [
                      "update",
                      "delete",
                      "set-role"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ],
                    "posts": [
                      "create",
                      "read",
                      "update",
                      "delete",
                      "update:own",
                      "delete:own"
                    ]
                  }
                }
              }
            }
          }
        },
        {
          "name": "email-otp",
          "config": {
            "id": "email-otp",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "$ERROR_CODES": {
              "OTP_EXPIRED": "otp expired",
              "INVALID_OTP": "Invalid OTP",
              "INVALID_EMAIL": "Invalid email",
              "USER_NOT_FOUND": "User not found",
              "TOO_MANY_ATTEMPTS": "Too many attempts"
            },
            "rateLimit": [
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              }
            ]
          }
        },
        {
          "name": "stripe",
          "config": {
            "id": "stripe",
            "endpoints": {},
            "schema": {
              "subscription": {
                "fields": {
                  "plan": {
                    "type": "string",
                    "required": true
                  },
                  "referenceId": {
                    "type": "string",
                    "required": true
                  },
                  "stripeCustomerId": {
                    "type": "string",
                    "required": false
                  },
                  "stripeSubscriptionId": {
                    "type": "string",
                    "required": false
                  },
                  "status": {
                    "type": "string",
                    "defaultValue": "incomplete"
                  },
                  "periodStart": {
                    "type": "date",
                    "required": false
                  },
                  "periodEnd": {
                    "type": "date",
                    "required": false
                  },
                  "trialStart": {
                    "type": "date",
                    "required": false
                  },
                  "trialEnd": {
                    "type": "date",
                    "required": false
                  },
                  "cancelAtPeriodEnd": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false
                  },
                  "seats": {
                    "type": "number",
                    "required": false
                  }
                }
              },
              "user": {
                "fields": {
                  "stripeCustomerId": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "next-cookies",
          "config": {
            "id": "next-cookies",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        }
      ]
    }
  }
}

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

Backend

Auth config (if applicable)

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

Additional context

stripe({
  stripeClient,
  stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
  createCustomerOnSignUp: true,
  subscription: {
    enabled: true,
    plans: [
      {
        name: "gold",
        priceId: process.env.GOLD_PRICE_ID,
        limits: {
          projects: 10,
          storage: 100,
          users: 5,
        },
      },
      {
        name: "platinum",
        priceId: process.env.PLATINUM_PRICE_ID,
        limits: {
          projects: 50,
          storage: 500,
          users: 25,
        },
      },
    ],
    onSubscriptionUpdate: async ({ event, subscription }) => {
      console.log(`Subscription ${subscription.id} updated:`, {
        plan: subscription.plan,
        status: subscription.status,
      });
    },
  },
});







```typescript
// Server action
export async function switchPlan(newPlan: "gold" | "platinum") {
  const headersList = await headers();
  const session = await auth.api.getSession({
    headers: headersList,
  });

  if (!session) {
    throw new Error("Not authenticated");
  }

  try {
    const currentSubscription = await getCurrentSubscription();

    if (!currentSubscription) {
      throw new Error("No active subscription found");
    }

    const result = await auth.api.upgradeSubscription({
      headers: headersList,
      body: {
        plan: newPlan,
        subscriptionId: currentSubscription.stripeSubscriptionId!,
        successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/plans`,
        cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/plans`,
      },
    });

    return { success: true, url: result.url };
  } catch (error: any) {
    return { success: false, error: error.message };
  }
}

Webhook Configuration

  • Webhook endpoint: /api/stripe/webhook
  • Webhook secret: Set in STRIPE_WEBHOOK_SECRET environment variable
  • Events: customer.subscription.updated, checkout.session.completed

Database Schema

model Subscription {
  id                   String    @id @default(uuid())
  plan                 String
  referenceId          String
  stripeCustomerId     String?
  stripeSubscriptionId String?
  status               String?
  periodStart          DateTime?
  periodEnd            DateTime?
  trialStart           DateTime?
  trialEnd             DateTime?
  cancelAtPeriodEnd    Boolean?  @default(false)
  seats                Int?

  @@map("subscription")
}
Originally created by @BABYB0I on GitHub (Sep 28, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4957 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a first subscription (e.g., Gold plan) 2. Try to update the subscription to a higher plan by passing the `subscriptionId` to the upgrade method 3. Check the database - the `subscription` table still shows the old plan ### Current vs. Expected behavior **Current behavior:** - Stripe creates a new subscription successfully - Database `subscription` table is not updated with the new plan - User remains on the old plan in the database **Expected behavior:** - Stripe updates the existing subscription - Database `subscription` table is updated with the new plan - User's subscription reflects the new plan ### What version of Better Auth are you using? 1.3.9 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Home", "release": "10.0.26100", "cpuCount": 12, "cpuModel": "13th Gen Intel(R) Core(TM) i7-1355U", "totalMemory": "15.69 GB", "freeMemory": "0.92 GB" }, "node": { "version": "v21.7.1", "env": "development" }, "packageManager": { "name": "bun", "version": "1.2.21" }, "frameworks": [ { "name": "next", "version": "15.5.2" }, { "name": "react", "version": "19.1.0" } ], "databases": [ { "name": "@prisma/client", "version": "^6.15.0" } ], "betterAuth": { "version": "^1.3.9", "config": { "emailAndPassword": { "enabled": true, "autoSignIn": false, "password": {}, "requireEmailVerification": true }, "emailVerification": { "sendOnSignUp": true, "autoSignInAfterVerification": true }, "socialProviders": { "google": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" }, "discord": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]" } }, "hooks": {}, "databaseHooks": { "user": { "create": {} } }, "user": { "additionalFields": { "role": { "type": [ "NONE", "PRODUCER", "ARTIST", "ADMIN", "OWNER" ], "input": false } } }, "session": { "expiresIn": 2592000 }, "account": { "accountLinking": { "enabled": false } }, "advanced": { "database": { "generateId": false } }, "plugins": [ { "name": "username", "config": { "id": "username", "endpoints": {}, "schema": { "user": { "fields": { "username": { "type": "string", "required": false, "sortable": true, "unique": true, "returned": true, "transform": {} }, "displayUsername": { "type": "string", "required": false, "transform": {} } } } }, "hooks": { "before": [ {}, {} ] }, "$ERROR_CODES": { "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]", "EMAIL_NOT_VERIFIED": "Email not verified", "UNEXPECTED_ERROR": "Unexpected error", "USERNAME_IS_ALREADY_TAKEN": "Username is already taken. Please try another.", "USERNAME_TOO_SHORT": "Username is too short", "USERNAME_TOO_LONG": "Username is too long", "INVALID_USERNAME": "Username is invalid", "INVALID_DISPLAY_USERNAME": "Display username is invalid" } } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "defaultRole": "NONE", "adminRoles": [ "ADMIN", "OWNER" ], "ac": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ], "posts": [ "create", "read", "update", "delete", "update:own", "delete:own" ] } }, "roles": { "NONE": { "statements": { "posts": [], "user": [ "set-role", "update" ] } }, "PRODUCER": { "statements": { "posts": [ "create", "read", "update:own", "delete:own" ], "user": [ "set-role" ] } }, "ARTIST": { "statements": { "posts": [ "create", "read", "update:own", "delete:own" ], "user": [ "set-role" ] } }, "ADMIN": { "statements": { "user": [ "update", "delete", "list", "set-role" ], "session": [ "list", "revoke", "delete" ], "posts": [ "create", "read", "update", "delete", "update:own", "delete:own" ] } }, "OWNER": { "statements": { "user": [ "update", "delete", "set-role" ], "session": [ "list", "revoke", "delete" ], "posts": [ "create", "read", "update", "delete", "update:own", "delete:own" ] } } } } } }, { "name": "email-otp", "config": { "id": "email-otp", "endpoints": {}, "hooks": { "after": [ {} ] }, "$ERROR_CODES": { "OTP_EXPIRED": "otp expired", "INVALID_OTP": "Invalid OTP", "INVALID_EMAIL": "Invalid email", "USER_NOT_FOUND": "User not found", "TOO_MANY_ATTEMPTS": "Too many attempts" }, "rateLimit": [ { "window": 60, "max": 3 }, { "window": 60, "max": 3 }, { "window": 60, "max": 3 }, { "window": 60, "max": 3 } ] } }, { "name": "stripe", "config": { "id": "stripe", "endpoints": {}, "schema": { "subscription": { "fields": { "plan": { "type": "string", "required": true }, "referenceId": { "type": "string", "required": true }, "stripeCustomerId": { "type": "string", "required": false }, "stripeSubscriptionId": { "type": "string", "required": false }, "status": { "type": "string", "defaultValue": "incomplete" }, "periodStart": { "type": "date", "required": false }, "periodEnd": { "type": "date", "required": false }, "trialStart": { "type": "date", "required": false }, "trialEnd": { "type": "date", "required": false }, "cancelAtPeriodEnd": { "type": "boolean", "required": false, "defaultValue": false }, "seats": { "type": "number", "required": false } } }, "user": { "fields": { "stripeCustomerId": { "type": "string", "required": false } } } } } }, { "name": "next-cookies", "config": { "id": "next-cookies", "hooks": { "after": [ {} ] } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context ```typescript stripe({ stripeClient, stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, createCustomerOnSignUp: true, subscription: { enabled: true, plans: [ { name: "gold", priceId: process.env.GOLD_PRICE_ID, limits: { projects: 10, storage: 100, users: 5, }, }, { name: "platinum", priceId: process.env.PLATINUM_PRICE_ID, limits: { projects: 50, storage: 500, users: 25, }, }, ], onSubscriptionUpdate: async ({ event, subscription }) => { console.log(`Subscription ${subscription.id} updated:`, { plan: subscription.plan, status: subscription.status, }); }, }, }); ```typescript // Server action export async function switchPlan(newPlan: "gold" | "platinum") { const headersList = await headers(); const session = await auth.api.getSession({ headers: headersList, }); if (!session) { throw new Error("Not authenticated"); } try { const currentSubscription = await getCurrentSubscription(); if (!currentSubscription) { throw new Error("No active subscription found"); } const result = await auth.api.upgradeSubscription({ headers: headersList, body: { plan: newPlan, subscriptionId: currentSubscription.stripeSubscriptionId!, successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/plans`, cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/plans`, }, }); return { success: true, url: result.url }; } catch (error: any) { return { success: false, error: error.message }; } } ``` ## Webhook Configuration - **Webhook endpoint**: `/api/stripe/webhook` - **Webhook secret**: Set in `STRIPE_WEBHOOK_SECRET` environment variable - **Events**: `customer.subscription.updated`, `checkout.session.completed` ## Database Schema ```prisma model Subscription { id String @id @default(uuid()) plan String referenceId String stripeCustomerId String? stripeSubscriptionId String? status String? periodStart DateTime? periodEnd DateTime? trialStart DateTime? trialEnd DateTime? cancelAtPeriodEnd Boolean? @default(false) seats Int? @@map("subscription") } ``` ```
GiteaMirror added the locked label 2026-04-17 18:28:16 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 28, 2025):

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

Your issue is caused by a breaking change in the Stripe API: fields like current_period_start and current_period_end were moved from the top level of the subscription object to subscription.items.data[0]. The Better Auth Stripe plugin v1.3.9 webhook handler expects these fields at the top level, so when Stripe sends a webhook, the handler tries to update the database with invalid dates, which causes the update to silently fail and leaves your subscription table stale (details).

How to fix:
Patch your webhook handler to support both old and new Stripe API versions. In your handler (usually in packages/stripe/src/hooks.ts or similar), update how you read the period fields:

const periodStart = subscription.current_period_start ?? subscription.items?.data?.[0]?.current_period_start;
const periodEnd = subscription.current_period_end ?? subscription.items?.data?.[0]?.current_period_end;

Then use periodStart and periodEnd for your database update. This will make your handler compatible with both API versions and restore correct DB updates (example).

If you can't patch locally, upgrading to the latest Better Auth and @better-auth/stripe versions will also resolve this for most users (source).

Other things to check:

  • Make sure your Stripe webhook endpoint is correctly configured and that events are being delivered (check Stripe dashboard and your server logs).
  • The onSubscriptionUpdate hook only logs updates; the actual DB update is handled by the webhook handler.
  • If you see subscriptions marked as active in the DB without payment, that's a known bug in v1.3.9 fixed in a later PR. Upgrading is recommended (source).

Let me know if you want a full code snippet or have questions about patching!

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:3344245820 --> @dosubot[bot] commented on GitHub (Sep 28, 2025): <!-- Greeting --> Hi @BABYB0I! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your issue is caused by a breaking change in the Stripe API: fields like `current_period_start` and `current_period_end` were moved from the top level of the subscription object to `subscription.items.data[0]`. The Better Auth Stripe plugin v1.3.9 webhook handler expects these fields at the top level, so when Stripe sends a webhook, the handler tries to update the database with invalid dates, which causes the update to silently fail and leaves your subscription table stale [(details)](https://github.com/better-auth/better-auth/issues/2087). **How to fix:** Patch your webhook handler to support both old and new Stripe API versions. In your handler (usually in `packages/stripe/src/hooks.ts` or similar), update how you read the period fields: ```js const periodStart = subscription.current_period_start ?? subscription.items?.data?.[0]?.current_period_start; const periodEnd = subscription.current_period_end ?? subscription.items?.data?.[0]?.current_period_end; ``` Then use `periodStart` and `periodEnd` for your database update. This will make your handler compatible with both API versions and restore correct DB updates [(example)](https://github.com/better-auth/better-auth/issues/2087). If you can't patch locally, upgrading to the latest Better Auth and @better-auth/stripe versions will also resolve this for most users [(source)](https://github.com/better-auth/better-auth/issues/4861#issuecomment-3336236575). **Other things to check:** - Make sure your Stripe webhook endpoint is correctly configured and that events are being delivered (check Stripe dashboard and your server logs). - The `onSubscriptionUpdate` hook only logs updates; the actual DB update is handled by the webhook handler. - If you see subscriptions marked as `active` in the DB without payment, that's a known bug in v1.3.9 fixed in a later PR. Upgrading is recommended [(source)](https://github.com/better-auth/better-auth/issues/3545#issuecomment-3102785614). Let me know if you want a full code snippet or have questions about patching! <!-- 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/365a9d19-8441-4151-8883-713fb01a2457?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/365a9d19-8441-4151-8883-713fb01a2457?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/365a9d19-8441-4151-8883-713fb01a2457?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/365a9d19-8441-4151-8883-713fb01a2457?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/365a9d19-8441-4151-8883-713fb01a2457?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/365a9d19-8441-4151-8883-713fb01a2457?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/365a9d19-8441-4151-8883-713fb01a2457?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/4957)
Author
Owner

@Bekacru commented on GitHub (Sep 30, 2025):

please upgrade to latest version as dosu suggested. If it's not fixed let me know

<!-- gh-comment-id:3350185913 --> @Bekacru commented on GitHub (Sep 30, 2025): please upgrade to latest version as dosu suggested. If it's not fixed let me know
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27445