Payment
Provider-based payment, checkout, billing, and webhook architecture.
Payment
Subscription and one-time payment support via a provider pattern. Switch between Stripe, Creem, PayPal, Paddle, and Alipay by setting VITE_PAYMENT_PROVIDER. Set it to '' (empty, the default) to disable payment entirely. All providers implement the same PaymentProvider interface, so downstream checkout, billing, webhooks, refunds, and order sync stay provider-agnostic. See Env for all variables.
Shared routes
- Pricing:
/pricing— plans and checkout buttons. - Payment callback:
/payment?session_id=...&callback=/settings/billing— polls until paid, then redirects. - Billing:
/settings/billing— current plan and subscription management.
Shared server API (Server Functions)
createCheckoutSession— create a checkout session, redirect URL returned.createCustomerPortalSession— create a billing portal session (Stripe Customer Portal / Creem customer portal).getCurrentPlan— current plan and subscription for a user.checkPaymentCompletion— whether a session is paid (for polling).
Module layout
| Path | Purpose |
|---|---|
src/payment/types.ts | PaymentProvider interface, shared types |
src/payment/index.ts | Provider factory/registry, exported functions |
src/payment/constants.ts | Polling/retry constants |
src/payment/provider/stripe.ts | Stripe provider implementation |
src/payment/provider/creem.ts | Creem provider implementation |
src/api/payment.ts | Server functions (provider-agnostic) |
src/lib/price-plan.ts | Plan/price helpers from config |
src/config/index.ts | Source-style price.defaultMarket, price.marketByLocale, price.markets, and env-based price/product IDs |
Stripe
Setup
-
Env: Set the following environment variables (see Env):
- Runtime (secrets):
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET - Build-time:
VITE_PAYMENT_PROVIDER=stripe,VITE_STRIPE_PRICE_PRO_MONTHLY,VITE_STRIPE_PRICE_PRO_YEARLY,VITE_STRIPE_PRICE_LIFETIME(Stripe Price IDs)
- Runtime (secrets):
-
DB: Customer IDs are stored on source-aligned payment business records (
order.payment_user_id/subscription.payment_user_id). They are not stored onuser.pnpm db:generate- Then apply with your D1 workflow (e.g.
pnpm db:migrate:remoteorpnpm db:migrate:local)
-
Stripe Dashboard:
- Create Products/Prices for Pro (monthly/yearly) and Lifetime.
- Webhook:
https://your-domain.com/api/webhook/stripeEvents:checkout.session.completed,customer.subscription.created|updated|deleted,invoice.paid.
Billing portal
Stripe provides a built-in Customer Portal for managing subscriptions (upgrade, cancel, update payment method). Accessed via the "Manage subscription" button on /settings/billing.
Creem
Creem is a merchant-of-record (MoR) payment platform. It handles global tax compliance, payouts, and provides a simpler setup compared to Stripe.
Setup
-
Env: Set the following environment variables (see Env):
- Runtime (secrets):
CREEM_API_KEY,CREEM_WEBHOOK_SECRET - Runtime (optional):
CREEM_DEBUG=trueto use the Creem test/sandbox API (test-api.creem.io); omit or set tofalsefor production (api.creem.io) - Build-time:
VITE_PAYMENT_PROVIDER=creem,VITE_CREEM_PRODUCT_PRO_MONTHLY,VITE_CREEM_PRODUCT_PRO_YEARLY,VITE_CREEM_PRODUCT_LIFETIME(Creem Product IDs)
- Runtime (secrets):
-
DB: Same storage rule as Stripe: customer IDs stay on payment business records, not on
user.pnpm db:generate- Then apply with your D1 workflow (e.g.
pnpm db:migrate:remoteorpnpm db:migrate:local)
-
Creem Dashboard:
- Create Products for Pro (monthly/yearly) and Lifetime.
- Webhook: Settings → Webhooks → Add endpoint:
https://your-domain.com/api/webhook/creemEvents:checkout.completed,subscription.paid,subscription.canceled,subscription.expired,subscription.trialing,subscription.paused.
Key differences from Stripe
- Creem uses Product IDs (not Price IDs) for checkout.
- Creem is a merchant of record — it handles tax, VAT, and payouts on your behalf.
- Creem provides a customer portal for subscription management, similar to Stripe's Customer Portal.
- Debug/sandbox mode is toggled via the
CREEM_DEBUGenv var.