DOCS LLMs

Stripe, payments & plans

Once your Stripe credentials are configured (like we did in the quickstart), setting up payments and pricing plans in your app is quite straightforward.

Install the Stripe CLI

The first thing you're gonna want to do is install the Stripe CLI and configure it with your Stripe Sandbox, so you can receive Stripe webhooks in development.

Once installed, log in to your Stripe account from the CLI:

stripe login

And then forward all development webhooks to your local dev server:

stripe listen --forward-to localhost:3000/pay/webhooks/stripe

You'll get a development Stripe webhook signing secret, which you need to add to your development credentials so that Stripe webhooks get processed.

With that, your development RailsFast app is now fully wired to Stripe, and reacting + processing all webhooks!

NOTE

For Stripe webhooks to get processed, Solid Queue needs to be running. bin/dev starts it for you (the jobs line in Procfile.dev). If you start your server another way, such as rails s, run the job worker yourself with bin/jobs.

Add and define your pricing plans

The next thing you're going to do is add your subscription options (your pricing plans) to your config/railsfast/railsfast.yml file. There's a placeholder there, you need to fill it up with your actual Stripe price IDs you've created in your Stripe dashboard:

development:
  <<: *default
  # Stripe pricing configuration (Sandbox/Test mode)
  # These are used by the pricing_plans gem (and by usage_credits, if you opt in)
  stripe:
    plans:
      starter:
        monthly: "price_REPLACE_ME_WITH_SANDBOX_PRICE"
        yearly: "price_REPLACE_ME_WITH_SANDBOX_PRICE"
      pro:
        monthly: "price_REPLACE_ME_WITH_SANDBOX_PRICE"
        yearly: "price_REPLACE_ME_WITH_SANDBOX_PRICE"
IMPORTANT

Make sure you use Stripe Sandbox price IDs for your development environment, and live price IDs for your production environment.

This is the single source of Stripe price IDs for your entire app: everything draws from here.

To have these subscription plans show up in your pricing page, edit the config/initializers/pricing_plans.rb file. RailsFast ships four plans that render on /pricing out of the box using plain price amounts: free, starter, pro (highlighted) and enterprise. Rename and reshape them into your own plans; when you're ready to charge through Stripe, swap each plan's price line for the commented stripe_price line so it reads the price IDs you configured above:

  stripe_config = Rails.configuration.x.railsfast.dig(:stripe, :plans) || {}

  plan :starter do
    price 9 # Comment out when using Stripe prices
    # stripe_price month: stripe_config.dig(:starter, :monthly), year: stripe_config.dig(:starter, :yearly)

    allows          :api_access
    limits          :projects, to: 5

    description     "Your starter plan description"
    bullets         "API Access", "Up to 5 projects"
  end

RailsFast uses the pricing_plans gem to define pricing plans. The first thing you get automatically is your users can purchase whatever plans you define (with monthly and yearly pricing) from your /pricing page. Try it out after defining your plans! On top of having checkout and billing already configured, whatever you define in each of your pricing plans is enforceable anywhere else in your app.

Subscriptions belong to the organization, not the user: every signup gets a personal workspace, and checkout, the billing portal and plan checks all run against current_organization. In the example above, a starter organization gets API access and up to 5 projects:

current_organization.plan_allows_api_access?  # => true / false
current_organization.projects_remaining       # => 2 (once Project is limited_by_pricing_plans)

Pricing plans & entitlements covers enforcement, per-account overrides, grandfathering, feature grants and trial passes, and the rule that makes the app refuse to boot on a misconfigured grace window.

RailsFast pricing cards with automatic pricing plan limits and entitlements enforcement

And with that, you're pretty much ready to start charging and providing a fully functional service! Both in development and production! Try it out!

If you want to tweak how the pricing page is rendered (toggle, accent color, scalable plans, etc.), see Pricing page.

Handle plan upgrades and downgrades

RailsFast uses the Stripe Customer Portal integration to offload everything billing-related to Stripe. This includes handling plan upgrades and downgrades, which Stripe does beautifully. To get started, go to your Stripe dashboard and configure your plan upgrade and downgrade options in: https://dashboard.stripe.com/settings/billing/portal

With that, your users will be able to upgrade or downgrade their subscription at any time through the Stripe Customer Portal link we provide with RailsFast. Everything else is automatically handled: whenever a user upgrades or downgrades, your app gets the webhook and your user gets moved to the corresponding plan, with its corresponding limits and enabled features.

When a renewal payment fails

A subscription that Stripe marks past_due keeps its plan while Stripe retries the payment. Settings and the admin show it as "Payment past due" in amber, with a prompt to update the payment method, instead of cutting the account off on the first failed charge. A Stripe subscription in the paused status (Stripe sets it when a trial ends without a card) answers paused? and resumable?, and resume bills the customer through Stripe.

Showing billing never creates a pay_customers row. Only checkout and the billing portal create one, because Stripe needs a customer there. If you write code of your own that only displays billing, read organization.stored_payment_processor (it returns nil when there is no row yet) instead of Pay's payment_processor, which creates a pay_customers row for every account it touches.

Handling Stripe errors in your own code

Since Pay 12, every Stripe failure inside a Pay call arrives as Pay::Stripe::Error, with the original Stripe error as its cause. A rescue Stripe::StripeError around a Pay call catches nothing. The template's checkout and billing portal redirects rescue Pay::Error and send the person back to Settings with a message; do the same around any checkout, billing_portal, charge, subscribe, pause, invoice! or sync call of yours:

begin
  current_organization.payment_processor.charge(1500)
rescue Pay::Error => e
  Rails.logger.error("Stripe refused: #{e.cause&.message || e.message}")
  redirect_to settings_path, alert: t(".charge_failed")
end

A rescue around a direct Stripe::... call of yours stays Stripe::StripeError.

One-time payments

Subscriptions are what RailsFast wires up end-to-end, but the underlying pay gem handles one-time charges out of the same customer/webhook plumbing you already have configured; see pay's charges guide. A dedicated RailsFast one-time-payments flow (checkout page + success screen) is on the roadmap.

Invoices

Stripe can email receipts and invoices for you with zero code: in your Stripe Dashboard, under Settings → Emails, enable emails for successful payments (make sure you set it in live mode too). A first-party receipts/invoicing integration in the template is on the roadmap.

Adding a production Stripe webhook

Don't forget to add a production Stripe webhook for your payments to work in production too!

  1. Go to your Stripe Dashboard - Webhooks (make sure it's in live mode, and not in Sandbox!)
  2. Click "Add endpoint"
  3. Enter your webhook URL: https://YOURDOMAIN.COM/pay/webhooks/stripe
  4. Select the events Pay handles: checkout.session.*, customer.* (which includes customer.subscription.*), invoice.*, charge.*, payment_intent.succeeded and payment_method.*. Selecting "receive all events" also works.
  5. Choose an API version of 2025-03-31 or later for the endpoint (see below).
  6. Copy the signing secret and add it to your production credentials, under production: → stripe: → signing_secret:. It is a list, so during a switch you can keep the old and the new endpoint's secrets side by side.

The endpoint's API version matters

Stripe pins an API version on every webhook endpoint when you create it, and builds the event objects in that version. Your app's own API calls are different: the stripe gem sends its own pinned version (2026-09-30.endive with stripe 20) on every request. Most Pay handlers fetch the object back from Stripe, so they see the current shape anyway, but invoices and checkout sessions are read straight from the event. Pay expects an invoice that names its subscription under parent.subscription_details, a shape Stripe introduced in API version 2025-03-31. On an older endpoint, Pay silently skips the payment-failed email and the invoice sync. Check the version under Developers → Webhooks in the Stripe dashboard and move an old endpoint forward following Stripe's webhook versioning guide.

Test the billing loop

The template ships a webhook lifecycle test (test/integration/pay_stripe_webhook_lifecycle_test.rb) that follows a subscription through checkout, a failed renewal, a successful retry and cancellation, without the network. Its offline Stripe helper, test/support/fake_stripe_api.rb, is yours to reuse in your own billing tests.

That test runs on Pay's own fixtures, so it cannot prove what your Stripe account sends today. Before you deploy a billing change, run one checkout and one cancellation in test mode against your own account, and confirm in Settings and in the admin that the subscription shows as active, then as canceled.

RailsFast uses the pay gem (12.x, with the stripe gem 20.x) to handle everything Stripe-related. Check out the Pay docs for guidance if what you want to do is not covered here.