Menu

Version 4.x Changelog

Good to know

  • Please check the version field in the package.json file 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_plans and pricing_plan_groups tables are dropped, and the admin pricing management pages (all files under /dashboard/admin/prices) are removed. config/pricing.ts becomes the single source of truth for pricing
  • Credit tables rebuilt: usage and credit_logs are dropped; credit_balances, organization_credit_balances, and credit_transactions are added
  • New organization tables: organization, member, invitation (Better Auth organization plugin)

Pricing: from database to code

  • Added: config/pricing.ts as 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.ts holds non-payment credit grant rules — currently the signup bonus signupBonusCredits (defaults to 30, set to 0 to disable)
  • Added: each plan declares who collects the money via its provider field, 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 / CheckoutButton trio; the former PricingAll / PricingByGroup / PricingByPaymentType / PricingCardDisplay / PricingCTA are removed
  • Convention: a planId is never reused and a plan that has been sold is never deleted (historical orders and credit ledger entries resolve through it); retire a plan with active: 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 former lib/payments/, app/api/*/webhook/handlers.ts, and app/api/payment/verify-success/* are all removed
    • types.ts: the unified CanonicalSubscriptionStatus and the fulfillment input shapes; amounts are always integer cents
    • fulfillment.ts: the provider-agnostic core — one-time purchase, subscription payment, subscription upgrade, subscription sync, subscription termination, refund
    • stripe.ts / creem.ts / paypal.ts: one adapter per provider, responsible only for translating webhook payloads into core inputs; SDK types never leak out
    • cancel.ts: cancelProviderSubscription, the unified cancel implementation, tolerating each provider's different "already cancelled" error
    • duplicate.ts: duplicate-subscription settlement — cancels the losing subscription, refunds it, and alerts (Redis-deduplicated); a failed refund throws so the provider retries
    • reconcile.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-deduplicated
    • customer.ts: the sole read/write channel for stripeCustomerId, 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, PermanentFulfillmentError alerts and then acks
  • Improved: Creem SDK upgraded from 0.4 to 1.6, @creem_io/nextjs removed, and lib/creem/client.ts plus the type definitions substantially slimmed down

Credits system rewrite

  • Refactored: added lib/credits/; balances can only change through ledger.ts, and credit_transactions is 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/credits scheduled 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/team team 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.tsx for switching between the personal and team workspaces
  • Added: email invitations to join a team, with the emails/organization-invitation.tsx template 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 RESTRICT foreign key as the database-level backstop
  • Convention: credit packs are not sold inside a team workspace; financial rows are SET NULL on user deletion and RESTRICT on 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/billing page — 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.tsx replaces the former SidebarUserNav and CurrentUserBenefitsDisplay, showing the credit balance directly in the sidebar
  • Added: the --credit theme color (coin gold), with its roles swapped between light and dark themes

Other

  • Added: the /api/health health 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 as skipped rather 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-gothic removed, and font usage unified across the Header, Footer, sidebar and other components
  • Improved: every CLAUDE.md in the repository and the code comments have been rewritten in English

Dependency upgrades

  • Next.js 16.1.116.3.3
  • React 19.2.319.2.8
  • creem 0.4.01.6.0 (@creem_io/nextjs removed)
  • cloudflare 6.4.07.1.0
  • Drizzle ORM 0.44 (PostgreSQL, public schema)
  • Better Auth 1.4 (organization plugin enabled)