Core Setup

Payments

Configure Stripe, Dodo Payments, or Polar for one-time purchases and subscriptions. Set up checkout, webhooks, and billing portal.

MoveFast supports three payment providers. Your variant comes with one of them pre-configured:

  • Stripe - The most popular payment processor
  • Dodo Payments - Merchant of record (handles taxes for you)
  • Polar - Built for developers and open source

All three support both one-time payments and recurring subscriptions through the same unified interface.


Payment Architecture

MoveFast uses a provider-agnostic design. All payment providers implement the same interface:

typescript
interface IPaymentProvider {
    createOneTimePayment(params) → CheckoutSession
    createSubscription(params) → CheckoutSession
    cancelSubscription(subscriptionId) → void
    getSubscription(subscriptionId) → SubscriptionDetails
    handleWebhookEvent(payload, signature) → WebhookHandleResult
    createBillingPortalSession(customerId) → { url }
}

This means the rest of your app never talks to Stripe/Dodo/Polar directly. It always goes through the service layer.


Payment Models

Configure your payment model in src/config/pricing.config.ts:

typescript
export const PAYMENT_MODEL: PaymentModel = PaymentModel.ONE_TIME;
// or
export const PAYMENT_MODEL: PaymentModel = PaymentModel.SUBSCRIPTION;

ONE_TIME: Users pay once and get lifetime access. The purchased planId is stored on the user record.

SUBSCRIPTION: Users pay monthly or yearly. A subscription record is created and managed through webhooks.


Configuring Pricing Plans

Edit the PRICING_PLANS array in src/config/pricing.config.ts:

typescript
export const PRICING_PLANS: PricingPlan[] = [
    {
        name: "Free",
        description: "Perfect for trying out",
        productIds: {
            monthly: "free",   // Special value, no checkout needed
            yearly: "free",
        },
        price: { monthly: 0, yearly: 0 },
        oneTimePrice: 0,
        features: [
            { name: "Feature one", included: true },
            { name: "Feature two", included: false },
        ],
        cta: { text: "Get Started", variant: "outline" },
    },
    {
        name: "Pro",
        productIds: {
            monthly: "your-monthly-product-id",
            yearly: "your-yearly-product-id",
            oneTime: "your-one-time-product-id",
        },
        price: { monthly: 29, yearly: 23 },
        oneTimePrice: 149,
        highlighted: true,
        badge: "Most Popular",
        features: [ ... ],
        cta: { text: "Start Free Trial", variant: "default" },
    },
];

Key points:

  • productIds must match the product/price IDs in your payment provider dashboard
  • For ONE_TIME model, use the oneTime field in productIds
  • For SUBSCRIPTION model, use the monthly and yearly fields
  • price.yearly is the per-month price when billed annually (for display)
  • Set highlighted: true to visually emphasize a plan
  • Set badge to show a label like "Most Popular"

Currency

typescript
export const DEFAULT_CURRENCY = "USD";   // Sent to payment provider (ISO 4217)
export const CURRENCY_SYMBOL = "$";      // Shown in the UI

Stripe Setup

1. Create an Account

Sign up at stripe.com and complete onboarding.

2. Get API Keys

Go to dashboard.stripe.com/apikeys and copy your secret key.

3. Environment Variables

env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

4. Create Products

Go to dashboard.stripe.com/products and create products for each plan. You do not need to create prices in Stripe since MoveFast creates them dynamically using price_data during checkout.

Use the product names as reference, then put any identifier in productIds in your pricing config (MoveFast uses these as metadata, not as Stripe price IDs).

5. Set Up Webhooks

In Stripe Dashboard > Developers > Webhooks, add an endpoint:

text
https://yourdomain.com/api/payment/webhook/stripe

Select these events:

  • payment_intent.succeeded
  • invoice.payment_succeeded
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted

Copy the webhook signing secret into STRIPE_WEBHOOK_SECRET.

6. Test Mode

Use sk_test_ keys during development. Switch to sk_live_ for production.


Dodo Payments Setup

1. Create an Account

Sign up at dodopayments.com.

2. Environment Variables

env
DODO_API_KEY=your-dodo-api-key
DODO_WEBHOOK_SECRET=your-dodo-webhook-secret
DODO_ENVIRONMENT=test_mode

3. Create Products

Create products in the Dodo dashboard. Copy each product ID into productIds in your pricing config.

4. Set Up Webhooks

Add this webhook endpoint in your Dodo dashboard:

text
https://yourdomain.com/api/payment/webhook/dodo

5. Switch to Production

When ready for real payments, change:

env
DODO_ENVIRONMENT=live_mode

Polar Setup

1. Create an Account

Sign up at polar.sh.

2. Create an Access Token

In your Polar dashboard, create an organization access token.

3. Environment Variables

env
POLAR_ACCESS_TOKEN=polar_at_...
POLAR_WEBHOOK_SECRET=your-webhook-secret
POLAR_ENVIRONMENT=sandbox

4. Create Products

Create products in the Polar dashboard. Copy each product ID into productIds in your pricing config.

5. Set Up Webhooks

Add this webhook endpoint in Polar:

text
https://yourdomain.com/api/payment/webhook/polar

Select these events:

  • order.paid
  • subscription.created
  • subscription.updated
  • subscription.active
  • subscription.canceled
  • subscription.revoked

6. Switch to Production

env
POLAR_ENVIRONMENT=production

How Checkout Works

  1. User clicks a pricing plan button on the frontend
  2. Frontend calls POST /api/payment/checkout with the productId
  3. Server validates the request and checks if the user already has an active subscription (for subscription model)
  4. Server calls the payment provider to create a checkout session
  5. Server returns the checkout URL
  6. Frontend redirects the user to the hosted checkout page
  7. After payment, user is redirected back to /payments
  8. The payment provider sends a webhook to confirm the payment
  9. Webhook handler creates a payment/subscription record in your database

How Webhooks Work

All three providers send webhook events to your app when something happens (payment completed, subscription cancelled, etc.). MoveFast normalizes these into 5 standard event types:

EventWhat it means
PAYMENT_COMPLETEDOne-time payment succeeded
SUBSCRIPTION_CREATEDNew subscription started
SUBSCRIPTION_UPDATEDSubscription renewed or plan changed
SUBSCRIPTION_CANCELLEDUser cancelled (may still be active until period end)
SUBSCRIPTION_DELETEDSubscription fully terminated

The WebhookCoordinator routes each event to either PaymentService or SubscriptionService for processing.

Billing Portal

For subscription users, MoveFast provides a "Manage Subscription" button that opens the payment provider's billing portal. Users can:

  • View their subscription details
  • Update payment method
  • Cancel or resume subscription
  • View invoice history

The billing portal URL is fetched via GET /api/subscription/billing-portal.


Testing Webhooks Locally

To test webhooks during development, you need to expose your local server to the internet.

For Stripe, use the Stripe CLI:

bash
stripe listen --forward-to localhost:3000/api/payment/webhook/stripe

For Dodo and Polar, use a tool like ngrok:

bash
ngrok http 3000

Then add the ngrok URL as your webhook endpoint in the provider dashboard temporarily.