LogoLaunchSaaS

Payment

Integrate Stripe or Creem payments in LaunchSaaS. Handle one-time purchases, subscriptions, webhooks, and customer billing with a unified API.

LaunchSaaS supports multiple payment providers with a unified, extensible architecture. You can easily switch between providers, add custom implementations, or leave payment unconfigured.

Supported Providers

  • Stripe - Industry-standard payment processor
  • Creem - Alternative payment provider
  • Custom - Extend with your own payment provider

Architecture Overview

The payment system consists of:

  • Order - Represents a confirmed monetary transaction (paid/irreversible)
  • Entitlement - Represents user access to a product
  • Payment Hooks - Extensible hooks for post-payment actions
  • Provider Factory - Manages payment provider instances

All payment providers follow the same responsibility split:

  • checkout.completed only creates order + entitlement for one-time payments
  • Subscription payment success events create the subscription order + activate entitlement
  • Other subscription.* events only sync entitlement status and dates

Configuration

Enable Payment Provider

Configure your payment provider in packages/config/src/features/development.ts or packages/config/src/features/production.ts:

// packages/config/src/features/development.ts
export const features: Features = {
  payment: {
    provider: "stripe", // "stripe" | "creem"
    hooks: {
      github: true, // Enable GitHub integration
      email: true, // Enable payment completion emails
    },
  },
  // ... other features
};

Omit the payment section entirely if your project does not use billing.

Setup Stripe

1. Create Stripe Account

  1. Sign up at stripe.com
  2. Complete account verification (required for production)
  3. You can start development with test mode immediately

2. Get API Keys

  1. Go to Stripe DashboardDevelopersAPI keys
  2. Copy your Secret key (starts with sk_test_ for test mode)
  3. Add to your .env file:
STRIPE_SECRET_KEY="sk_test_..."

Use test keys (sk_test_...) for development and live keys (sk_live_...) for production.

3. Create Products and Prices

  1. Go to Stripe DashboardProduct Catalog
  2. Click Add product
  3. Fill in product details:
    • Name: e.g., "Lifetime Access"
    • Description: Your product description
    • Price: Set your price (one-time or recurring)
  4. Click "Save product"
  5. Copy the Price ID (starts with price_)

4. Configure Products

Update the product configuration in packages/config/src/product/development.ts or packages/config/src/product/production.ts:

// packages/config/src/product/production.ts
export const productConfiguration: ProviderProductConfiguration = {
  stripe: {
    onetime: [
      {
        id: "price_1SWtddRwnUQyjRPerb8ge9xD",
        name: "Lifetime Access",
        hooks: ["github-integration", "payment-completed-email"],
      },
    ],
    subscription: [
      {
        id: "price_1SOwEE2Kn68A5jDtD9vcpirC",
        name: "Pro Monthly",
        hooks: ["payment-completed-email"],
      },
    ],
    pricingPageProduct: {
      type: "onetime",
      id: "price_1SWtddRwnUQyjRPerb8ge9xD",
    },
  },
};

5. Set Up Webhooks

Webhooks notify your application of payment events in real-time.

Development (Local Testing)

Use the Stripe CLI to forward webhooks to your local server:

  1. Install Stripe CLI:
# macOS
brew install stripe/stripe-cli/stripe

# Other platforms: https://stripe.com/docs/stripe-cli
  1. Login to Stripe:
stripe login
  1. Forward webhooks:
stripe listen --forward-to localhost:3000/api/payment/stripe/webhook
  1. Copy the webhook secret (starts with whsec_) and add to your .env file:
STRIPE_WEBHOOK_SECRET="whsec_..."

Keep the stripe listen command running while developing.

Production

  1. Go to Stripe DashboardDevelopersWebhooks
  2. Click Add endpoint
  3. Set endpoint URL: https://yourdomain.com/api/payment/stripe/webhook
  4. Select events to listen to:
    • checkout.session.completed
    • invoice.paid
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
  5. Click Add endpoint
  6. Copy the Signing secret and add to your production environment

Required Events

Configure your Stripe webhook endpoint to listen to exactly these events:

  • checkout.session.completed
  • invoice.paid
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted

Stripe Webhook Flow

LaunchSaaS uses a split event model with three independent responsibility lanes, so event delivery order does not matter.

One-Time Payment Flow

Subscription Flow

Event Responsibilities

EventAction
checkout.session.completed (mode=payment)Create order + active entitlement (one-time)
checkout.session.completed (mode=subscription)Ignored — subscriptions handled by invoice.paid
invoice.paidCreate order + upsert entitlement to active
customer.subscription.created (trialing)Create entitlement with status trialing
customer.subscription.created/updatedUpdate entitlement: period, status, cancellation (no-op if not yet created)
customer.subscription.deletedUpdate entitlement status to canceled

Why the flow is split this way

  • checkout.session.completed with mode=payment is the unambiguous one-time purchase signal.
  • invoice.paid is the authoritative payment success signal for subscriptions — it upserts the entitlement to active regardless of what other events have fired.
  • customer.subscription.* handles two cases: for trialing subscriptions it creates the entitlement (since no invoice.paid fires during a trial); for all other statuses it only updates existing records and never upgrades to active, so it cannot cause incorrect access grants even if it arrives before invoice.paid.

This keeps entitlement updates correct even when Stripe delivers events out of order.

Idempotency

RecordKey
Order (one-time)stripe_${payment_intent.id}
Order (subscription)stripe_${invoice.id}
Entitlement (one-time)stripe_pi_${payment_intent.id}
Entitlement (subscription)stripe_${subscription.id}

Replayed events are safe — duplicate inserts are silently ignored.

Testing Cards

For testing Stripe integration, use test credit cards:

Card NumberDescription
4242 4242 4242 4242Successful payment
4000 0000 0000 32203D Secure required
4000 0000 0000 9995Insufficient funds

Use any future expiration date and any 3-digit CVC.

Setup Creem

1. Create Creem Account

  1. Sign up at creem.io
  2. Complete account verification
  3. Create your products

2. Get API Keys

  1. Go to Creem Dashboard → Settings → API Keys
  2. Copy your API key and webhook secret
  3. Add to your .env file:
CREEM_API_KEY="your-api-key"
CREEM_WEBHOOK_SECRET="your-webhook-secret"

3. Configure Products

Update the product configuration in packages/config/src/product/development.ts or packages/config/src/product/production.ts:

// packages/config/src/product/production.ts
export const productConfiguration: ProviderProductConfiguration = {
  creem: {
    onetime: [
      {
        id: "prod_3h218lKmpAIM9xTv97Mdy7",
        name: "Lifetime Access",
        hooks: ["github-integration", "payment-completed-email"],
      },
    ],
    pricingPageProduct: {
      type: "onetime",
      id: "prod_3h218lKmpAIM9xTv97Mdy7",
    },
  },
};

4. Set Up Webhooks

Configure webhook endpoint in Creem Dashboard:

  • URL: https://yourdomain.com/api/payment/creem/webhook
  • Events: All payment-related events

If you are setting up the environment, now you can go back to the Environment Setup guide and continue.

Payment Hooks

LaunchSaaS provides an extensible hook system for post-payment actions. Hooks are configured in packages/config/src/features/.

Built-in Hooks

GitHub Integration

Automatically adds customers as GitHub collaborators after payment.

Enable in packages/config/src/features/development.ts or packages/config/src/features/production.ts:

payment: {
  hooks: {
    github: true,
  },
}

Configure environment variables:

GITHUB_TOKEN="your-personal-access-token"
GITHUB_REPO="owner/repo"

Generate a GitHub Personal Access Token with admin:org and repo scopes at GitHub Settings

Payment Completed Email

Sends confirmation emails after successful payments.

Enable in packages/config/src/features/development.ts or packages/config/src/features/production.ts:

payment: {
  hooks: {
    email: true,
  },
}

Custom Hooks

Create custom payment hooks by implementing the PaymentHook interface in packages/payment/src/hooks/:

import { PaymentHook, AfterHookContext } from "@/lib/payment/hook";

export class CustomHook implements PaymentHook {
  name = "custom-hook";

  async onCheckoutComplete(context: AfterHookContext): Promise<void> {
    // Your custom logic here
    console.log(`Payment completed for product ${context.productId}`);
  }
}

Register your hook in packages/payment/src/hook-register.ts:

if (features.payment.hooks.custom) {
  hooks.push(new CustomHook());
}

Database Schema

Order Table

Represents confirmed monetary transactions:

  • Created when payment is confirmed
  • One-time products create an order upon checkout completion
  • Subscription products create an order upon each successful payment

Entitlement Table

Represents user access to products:

  • Granted/extended when order is paid
  • One-time products create entitlement with status "active"
  • Subscription products create entitlement with status "pending", becomes "active" after first payment
  • Contains period information for subscriptions

Custom Payment Provider

To add a custom payment provider:

  1. Implement the PaymentProvider interface in packages/payment/src/providers/
  2. Register in packages/payment/src/factory.ts
  3. Add to the Features type in packages/config/src/schemas/site-configuration.ts

Example:

export class CustomProvider implements PaymentProvider {
  readonly name = "custom";

  async createCheckout(options: CheckoutOptions): Promise<CheckoutResult> {
    // Implement checkout creation
  }

  async handleWebhook(request: Request): Promise<Response> {
    // Implement webhook handling
  }

  // ... other required methods
}

References

Next Steps