Dynamic Pricing
Manage pricing plans in real time from the admin panel, without code changes or redeployment
Dynamic pricing lets you create, edit, and adjust pricing plans directly in the admin panel — no code changes or redeployment needed. The default static pricing (config/payment.ts) is unaffected, and you can switch between the two modes anytime.
Enable Dynamic Pricing
-
Set in
.env:PRICING_MODE=dynamic -
Make sure the
pricing_plantable exists. New versions of the template ship with this table in the schema — a normal database init (pnpm db:push) already includes it. Projects upgraded from older versions need to add the table manually:pnpm db:push # PostgreSQL pnpm db:push:sqlite # SQLite / D1 -
Restart the app. The pricing page now reads plans from the database.
PRICING_MODE only takes effect on the server. When unset or set to static, behavior is exactly the same as before.
Admin Panel
With dynamic pricing enabled, manage all plans at /admin/pricing:
- Create / edit plans: set the name, price, currency, type (subscription / lifetime / credit pack), and the provider's Price ID / Product ID
- Enable / disable: toggle whether a plan appears on the pricing page
- Reorder: drag to adjust the display order on the pricing page
- One-click import: import existing static plans from
config/payment.tsinto the database — ideal for the first migration
Import only creates new records and never overwrites existing plans. Importing repeatedly produces duplicates.
Key Capabilities
Compared to static pricing, dynamic mode adds:
- Strikethrough pricing: set an
originalPriceto show the original price struck through with the discount - Multilingual content: each plan can have per-language names, descriptions, and features, with automatic fallback to English
- Markdown features: the Features field supports Markdown for rich-text highlights
- Locale filtering: restrict a plan to specific locales (e.g.
zh-CNonly); the pricing page filters automatically by user language. Plans without a locale are visible to everyone - Custom ordering: control display order via sort weight instead of code order
Locale filtering only controls what the pricing page displays — users can still purchase plans from other regions via the API directly.
Migrating from Static to Dynamic
This section is only for existing projects upgraded from an older version. New projects already have the pricing_plan table in their schema — just set PRICING_MODE=dynamic and import or create plans in the admin panel; skip step 1.
- Run
pnpm db:push(orpnpm db:push:sqlite) to add thepricing_plantable - Click "Import from Config" at
/admin/pricingto import existing plans - Review the import, especially each provider's Price / Product ID
- Set
PRICING_MODE=dynamicand restart - Run a full payment flow in test mode to confirm the pricing page and Webhook callbacks work
Rollback: set PRICING_MODE back to static and restart. Plans in the database are preserved.
Notes
- Dynamic plans must have the correct provider ID, or payments will fail:
stripePriceIdfor Stripe,creemProductIdfor Creem,dodoProductIdfor Dodo,waffoProductIdfor Waffo (PROD_xxx). WeChat Pay / Alipay need none - Fully compatible with the affiliate system — commission is calculated from the actual paid amount, regardless of pricing mode
- Run
pnpm db:seedto generate sample plans for each provider (including Stripe, WeChat Pay, Creem, PayPal, Dodo, Waffo). If plans already exist, seed skips insertion — add new ones manually in the admin panel
Reference
- Payment Overview — payment methods and static pricing configuration
- Credits Guide — credit top-up and consumption configuration
- Payment Testing Guide — local testing and Webhook debugging