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

PathPurpose
src/payment/types.tsPaymentProvider interface, shared types
src/payment/index.tsProvider factory/registry, exported functions
src/payment/constants.tsPolling/retry constants
src/payment/provider/stripe.tsStripe provider implementation
src/payment/provider/creem.tsCreem provider implementation
src/api/payment.tsServer functions (provider-agnostic)
src/lib/price-plan.tsPlan/price helpers from config
src/config/index.tsSource-style price.defaultMarket, price.marketByLocale, price.markets, and env-based price/product IDs

Stripe

Setup

  1. 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)
  2. DB: Customer IDs are stored on source-aligned payment business records (order.payment_user_id / subscription.payment_user_id). They are not stored on user.

    • pnpm db:generate
    • Then apply with your D1 workflow (e.g. pnpm db:migrate:remote or pnpm db:migrate:local)
  3. Stripe Dashboard:

    • Create Products/Prices for Pro (monthly/yearly) and Lifetime.
    • Webhook: https://your-domain.com/api/webhook/stripe Events: 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

  1. Env: Set the following environment variables (see Env):

    • Runtime (secrets): CREEM_API_KEY, CREEM_WEBHOOK_SECRET
    • Runtime (optional): CREEM_DEBUG=true to use the Creem test/sandbox API (test-api.creem.io); omit or set to false for 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)
  2. 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:remote or pnpm db:migrate:local)
  3. Creem Dashboard:

    • Create Products for Pro (monthly/yearly) and Lifetime.
    • Webhook: Settings → Webhooks → Add endpoint: https://your-domain.com/api/webhook/creem Events: 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_DEBUG env var.
xs