Version 4.x Changelog
Good to know
- Please check the
versionfield in thepackage.jsonfile to get the version number- For the 3.x changelog, please refer to Version 3.x Changelog
4.0.0
4.0.0 is a rewrite-level upgrade: payments, credits, and teams have been completely redone, pricing data has moved from the database into code, and the database migration files have been rebuilt from a new baseline. Upgrading from 3.x takes more than a pnpm install — read the "Breaking Changes" section below first.
Breaking Changes (must-read when upgrading from 3.x)
- Pricing is no longer stored in the database: the
pricing_plansandpricing_plan_groupstables are dropped, and the admin pricing management pages (all files under/dashboard/admin/prices) are removed.config/pricing.tsbecomes the single source of truth for pricing - Credit tables rebuilt:
usageandcredit_logsare dropped;credit_balances,organization_credit_balances, andcredit_transactionsare added - New organization tables:
organization,member,invitation(Better Auth organization plugin)
Pricing: from database to code
- Added:
config/pricing.tsas the single source of truth for pricing. One plan = one object, spelling out the provider, the provider-side price ID ({ test, live }for both environments), the credits it grants, and the en/zh/ja copy — no longer scattered across i18n files - Added:
config/credits.tsholds non-payment credit grant rules — currently the signup bonussignupBonusCredits(defaults to 30, set to 0 to disable) - Added: each plan declares who collects the money via its
providerfield, and fills in the matching field per provider:stripePriceId/creemProductId/paypalPlanId - Added:
scripts/bootstrap-stripe-prices.ts(pnpm stripe:bootstrap), which batch-creates prices on the Stripe side from the config file - Improved: pricing components rewritten as the
PricingSection/PricingCard/CheckoutButtontrio; the formerPricingAll/PricingByGroup/PricingByPaymentType/PricingCardDisplay/PricingCTAare removed - Convention: a
planIdis never reused and a plan that has been sold is never deleted (historical orders and credit ledger entries resolve through it); retire a plan withactive: false
Payment fulfillment: the unified lib/billing
- Refactored: added
lib/billing/, which funnels webhook events from all three providers into one provider-agnostic fulfillment core. The formerlib/payments/,app/api/*/webhook/handlers.ts, andapp/api/payment/verify-success/*are all removedtypes.ts: the unifiedCanonicalSubscriptionStatusand the fulfillment input shapes; amounts are always integer centsfulfillment.ts: the provider-agnostic core — one-time purchase, subscription payment, subscription upgrade, subscription sync, subscription termination, refundstripe.ts/creem.ts/paypal.ts: one adapter per provider, responsible only for translating webhook payloads into core inputs; SDK types never leak outcancel.ts:cancelProviderSubscription, the unified cancel implementation, tolerating each provider's different "already cancelled" errorduplicate.ts: duplicate-subscription settlement — cancels the losing subscription, refunds it, and alerts (Redis-deduplicated); a failed refund throws so the provider retriesreconcile.ts: cron-driven reconciliation that recovers lost webhooks (renewal replay + advancing pending PayPal captures)notify.ts: webhook-triggered notifications (grant failure alerts to admins, renewal failure notices to users, fraud alerts), all Redis-deduplicatedcustomer.ts: the sole read/write channel forstripeCustomerId, self-healing on a stale ID
- Added: subscription upgrades (cross-plan changes) go through incremental fulfillment, granting the credit difference, plus a subscription upgrade notification
- Added: pending payments such as PayPal eCheck first land as
status='pending'order rows, which cron then advances to success or failure - Added: the admin orders page supports refunds (
RefundDialog) - Improved: idempotency is structural — the unique index on
orders(provider, provider_order_id)+onConflictDoNothing+ cumulative refund totals, instead of defensive checks - Improved: a refund rewrites the original order row rather than adding a new one
- Improved: webhook routes stay thin — retryable errors return 5xx,
PermanentFulfillmentErroralerts and then acks - Improved: Creem SDK upgraded from 0.4 to 1.6,
@creem_io/nextjsremoved, andlib/creem/client.tsplus the type definitions substantially slimmed down
Credits system rewrite
- Refactored: added
lib/credits/; balances can only change throughledger.ts, andcredit_transactionsis an append-only log (delta + snapshot + sequence) - Added: a two-bucket model — subscription credits (reset on each grant, cleared when the subscription ends) and purchased credits (never expire); spending drains the subscription bucket first
- Added: monthly drip for annual plans — month one is granted immediately and the remaining 11 drip out monthly; cron and lazy settlement share the same entry point, and a row lock plus recheck guarantees a concurrent trigger grants only once
- Added: the
app/api/cron/creditsscheduled entry point (POST+Authorization: Bearer $CRON_SECRET), which settles credit drips, reconciles overdue renewals, and reconciles pending PayPal captures - Added: signup bonus credits, checked for duplicates inside the same transaction so replays are harmless
- Added:
app/[locale]/(protected)/dashboard/credit-usage-example/, a credit spending example - Improved: balances never go negative; clawback operations clamp at 0
Teams / Organizations
- Added: team support built on the Better Auth organization plugin — the organization is the billing subject and shares one credit pool (
organization_credit_balances) - Added: the
/dashboard/teamteam management page — four cards for subscription, shared credit pool, members and seats, and the credit ledger; without a team, a create/select onboarding flow takes over - Added:
components/shared/WorkspaceSwitcher.tsxfor switching between the personal and team workspaces - Added: email invitations to join a team, with the
emails/organization-invitation.tsxtemplate and the/accept-invitation/[id]acceptance page - Added: the seat cap equals the team plan's
seats, or 1 without a subscription (holding only the owner). Four Better Auth hooks run the pre-checks, backed by an advisory lock plus over-cap reclamation to cover concurrency - Added: an organization deletion guard — deletion is refused while a live subscription or any funding history exists, with the
RESTRICTforeign key as the database-level backstop - Convention: credit packs are not sold inside a team workspace; financial rows are
SET NULLon user deletion andRESTRICTon organization deletion
Admin dashboard
- Added: a coupon console at
/dashboard/admin/coupons. Stripe is the only store — no local table, no migration: one coupon = a Stripe Coupon (discount, product scope, billing duration) + a Stripe Promotion Code (the customer-facing code, redemption cap, expiry, restrictions). A code is immutable once created, so the UI only offers enable/disable and "duplicate, then edit" - Added: a credits management page at
/dashboard/admin/credits— a global credit ledger audit view plus batch grants by email (deduplicated, max 200 per call, with unmatched emails reported back) - Added: a user detail page at
/dashboard/admin/users/[userId]— a 360° view of one user: profile, credit balances, subscription, lifetime stats and history, with manual credit adjustment - Added: a signup email blocklist — the admin dashboard maintains three kinds of rules (prefix, suffix, exact address) stored in Redis; the signup hook compares against both the raw lowercased form and the normalized form, defeating Gmail dot and
+suffix bypasses. A failed read silently degrades to empty rules and never blocks legitimate signups - Added: deleting a user account first purges that user's R2 assets (
lib/storage/account-purge.ts); if the purge fails, the deletion is aborted - Redesigned: the Overview page. A single time window (
?range=7d|30d|90d, UTC calendar days) drives the whole page, and five sections stream in parallel under their own Suspense boundaries so one failing section cannot take down the page- KPI row: net revenue, MRR, new signups, credit burn
- Trend chart: a single-series bar chart with metric tabs (switching does not refetch)
- Needs attention: past-due subscriptions, pending orders, cancel-at-period-end, open feedback, failed orders, off-currency orders — each row links straight to its admin page
- Distribution cards + the 8 most recent orders
- Conventions: amounts are integer cents, net revenue = total − refunded, pending/failed do not count as revenue, the reporting currency comes from the first active plan in the pricing config, signups exclude anonymous accounts, credit burn = usage − usage refunds, and MRR = active + past_due at the configured monthly rate
- Improved: the data table container and the layouts across admin pages are unified, with better mobile responsiveness
User dashboard
- Added: the
/dashboard/billingpage — two credit buckets, the current plan, purchase history and the credit ledger (each table paginated independently). In a team workspace, a top banner points to/dashboard/team - Redesigned:
components/layout/SidebarUserCard.tsxreplaces the formerSidebarUserNavandCurrentUserBenefitsDisplay, showing the credit balance directly in the sidebar - Added: the
--credittheme color (coin gold), with its roles swapped between light and dark themes
Other
- Added: the
/api/healthhealth check endpoint, which probes every backing service in parallel and folds the results into a single HTTP status (200 / 503); an unconfigured dependency is recorded asskippedrather than a failure. Deliberately public and deliberately opaque: it takes no secret, and it returns no error strings, versions, or hostnames - Added:
components/shared/DomTranslationGuard.tsx, a defensive guard against React DOM crashes caused by whole-page browser translation - Improved: the font system has been tidied up,
@fontsource/science-gothicremoved, and font usage unified across the Header, Footer, sidebar and other components - Improved: every
CLAUDE.mdin the repository and the code comments have been rewritten in English
Dependency upgrades
- Next.js
16.1.1→16.3.3 - React
19.2.3→19.2.8 - creem
0.4.0→1.6.0(@creem_io/nextjsremoved) - cloudflare
6.4.0→7.1.0 - Drizzle ORM 0.44 (PostgreSQL,
publicschema) - Better Auth 1.4 (organization plugin enabled)