Skip to main content
Use this guide when upgrading an existing @creem_io/better-auth integration. For a new integration, start with the quickstart.

Upgrading from 1.x

Version 2.0 changes checkout navigation and endpoint error handling, improves native client types, and distinguishes scheduled cancellation from final cancellation in webhook callbacks and persisted access checks. Review the client, server, and webhook changes below before deploying. No database schema migration is required for unchanged default persistence settings. If you use custom schema mappings, review the mapping changes below against your existing database.

Upgrade Better Auth alongside the plugin

Version 2.0 requires Better Auth ^1.5.6. The native inferred endpoint declarations depend on Better Auth types that are not available at the previous minimum of 1.3.34. Upgrade both packages together, including in separate server and client applications:

Update checkout and portal navigation

Checkout and portal methods now default to redirect: false. Existing calls that relied on automatic navigation must explicitly opt in:
For manual navigation, omit redirect or set it to false, handle error, and navigate using data.url:

Migrate to the native client

createCreemAuthClient is deprecated but remains exported. Replace it with the standard Better Auth client for your framework and keep the Creem client plugin:
The native client infers inputs and successful responses for all six Creem methods, plus session.user.creemCustomerId and session.user.hadTrial when persistence is enabled. Error payloads no longer appear as alternatives inside the successful data type, so checks such as "url" in data are unnecessary after handling error. Native types follow the endpoint’s actual SDK response. Customer or product values can be IDs or expanded objects; narrow them before reading properties. Date values received over HTTP are serialized, not JavaScript Date instances. For example:

Update transaction response paths

The Better Auth searchTransactions() endpoint now returns the transaction page directly as { items, pagination }, matching the documented client contract. If you previously used the native client or raw endpoint’s SDK iterator envelope, change data.result.items to data.items and data.result.pagination to data.pagination. The HTTP endpoint does not expose iterator methods. The standalone helper from @creem_io/better-auth/server is unchanged.

Update endpoint error handling

Failures from createCheckout, createPortal, cancelSubscription, retrieveSubscription, searchTransactions, and hasAccessGranted now use Better Auth’s APIError mechanism. Existing HTTP status codes are preserved, but the error contract changes: Do not check data.error or assume a failed direct server call resolves to an error object. hasAccessGranted() returns data.hasAccessGranted as a boolean on success. Signed-out requests (401), disabled persistence (400), and failed checks (500) use the error path with data: null instead of an indeterminate access result. Treat an error as a failed check, not authorization. See the Server API for a try/catch example. Helpers imported from @creem_io/better-auth/server are separate from these Better Auth endpoints and retain their own behavior.

Match persistence configuration and review schema mappings

The full persistence opt-out remains supported. A server configured with persistSubscriptions: false registers neither subscription models nor Creem user fields and performs no plugin customer, trial, or subscription writes. Mirror that setting in the client so its session types match the server:
This client option controls inference only; it does not change the server’s behavior. Leave both plugins at their defaults when using persistence. If you store billing data yourself, read it through your own typed data layer instead of expecting Creem fields on the plugin session. The schema option now correctly types physical name mappings for existing plugin models and fields. Field overrides are column-name strings, not Better Auth field definition objects:
Update any object-shaped field overrides to string mappings. This option does not add models or fields or change their logical types, and it is ignored when persistence is disabled. Each plugin instance now applies overrides independently. If you change physical names during the upgrade, check your generated ORM schema and apply the corresponding database migration before deployment.

Cancellation and access changes

Access behavior in this table refers to hasAccessGranted() with persistence enabled. The existing past_due and unpaid period-end grace behavior is unchanged. The direct server helpers retain their documented active-status scope.

Update your callbacks

Add subscription_canceled to any exhaustive switches or mappings over RevokeAccessReason or AccessChangeReason. If your onRevokeAccess handler already handles every reason the same way, it will now also run for final cancellation. If you currently revoke access in onSubscriptionCanceled, move that logic into onRevokeAccess, or ensure that calling both handlers has the same effect as calling one. Keep handlers idempotent because webhooks can be delivered more than once. These callback changes also apply when persistSubscriptions: false. Scheduling cancellation does not invoke onGrantAccess or onRevokeAccess. If you manage entitlements outside the plugin database, use onSubscriptionScheduledCancel to store the period end and enforce it in your own access checks. See Webhooks and access for callback examples and retry behavior.

Review existing records and entitlements

The new access policy applies to existing records as soon as you deploy 2.0. Upgrading does not replay webhooks or invoke onRevokeAccess for records already stored as canceled. Before deploying:
  1. Review local canceled records that currently grant access and compare them with the subscription’s current state in Creem. Correct stale local state from that source; do not relabel a canceled subscription as scheduled solely because its stored period end is in the future.
  2. If you store entitlements separately, reconcile them with the current subscription state. Do not rely on a new revoke callback arriving solely because you upgraded the package.
  3. Check any custom use of cancelAtPeriodEnd. Existing flags are not automatically backfilled; subsequent subscription webhooks and checkout writes synchronize the flag. The plugin’s access checks use status and periodEnd.
An existing canceled record can still have a future periodEnd. In this edge case, upgrading stops that record from granting access immediately, rather than waiting for the stored date.

Install and verify

After updating your handlers and reviewing existing state, install the new major version:
Run your application’s typecheck to catch native response types, persistence options, schema mappings, and exhaustive reason checks that need updating. Verify manual and automatic checkout/portal navigation, client error handling, and any direct auth.api error handling. In test mode, verify that scheduled cancellation preserves access before the period end, that access ends at the period boundary, and that final cancellation invokes your revoke logic without duplicate side effects. If you manage entitlements separately, verify those access checks too.