Pro Adding a Subscription
Viewing latest docs.
Switch version: v3

Adding a subscription

How to start charging customers in your Petal Pro app.

Before you start

This guide picks up from where the main tutorial leaves off — you should have a Petal Pro app running locally. If you don’t, follow that guide first to get a fresh app set up. Or just unzip Petal Pro into a fresh folder, run mix setup, start the server, and you’re ready.

You’ll also need:

ℹ️ Information: Stripe now offers Sandboxes as an alternative to test mode. We recommend using a Sandbox — it's an isolated environment so you can experiment without polluting your real account data.

Configure Stripe

Make sure Stripe is in test mode (or use a Sandbox), then sign in via the CLI:

shell
stripe login

Create your products

Petal Pro comes pre-configured for three plans: Essential, Business, and Enterprise with monthly and yearly prices. You’ll create those products in Stripe and grab the IDs.

Create the Essential product:

shell
stripe products create --name="Essential"

Copy the id from the response — it looks like prod_xxxxx.

Create the monthly price:

shell
stripe prices create \
  --unit-amount=1900 \
  --currency=usd \
  -d "recurring[interval]"=month \
  --product="prod_xxxxx"

ℹ️ Information: --unit-amount is in cents, not dollars.

Now the yearly price:

shell
stripe prices create \
  --unit-amount=19900 \
  --currency=usd \
  -d "recurring[interval]"=year \
  --product="prod_xxxxx"

Repeat the whole process for Business and Enterprise. Suggested pricing:

Product Essential Business Enterprise
Monthly $19 $49 $99
Yearly $199 $499 $999

You’ll end up with six prices in total. Keep them somewhere — you’ll paste them into your Petal Pro config shortly.

Allow plan switching in the Customer Portal

When users want to change plans, Stripe handles it through the Customer Portal. You need to enable plan switching:

  1. In Stripe (test mode), click the cog (top right) → Settings
  2. In Billing, click Customer Portal
  3. Expand Subscriptions
  4. Enable Customers can switch plans
  5. Add prices from Essential, Business, and Enterprise via “Find a test product…”

You might also want to fill in Business information to customise the portal’s branding.

Configure Petal Pro

Three things to do locally:

  1. Run the Stripe webhook listener
  2. Add Stripe secrets to your environment
  3. Update the Petal Pro config with your product/price IDs

Webhook listener

shell
stripe listen --forward-to localhost:4000/webhooks/stripe

Output will include a webhook signing secret like whsec_xxxxxxxxxxxxxxx. Save it — that’s your STRIPE_WEBHOOK_SECRET.

Keep this command running in a terminal (or in tmux). If you’re using Claude Code, ask:

run the stripe webhook listener in tmux so it keeps going in the background

Environment variables

You need:

  • STRIPE_SECRET — get it from Stripe → DevelopersAPI keysReveal test key
  • STRIPE_WEBHOOK_SECRET — from the stripe listen output above

Petal Pro uses direnv for local environment variables. Install it, then:

shell
cp .envrc.example .envrc

Uncomment and fill in the Stripe variables at the bottom of .envrc. The .envrc file is gitignored — your secrets stay local.

Activate them:

shell
direnv allow

Update the product config

Open config/config.exs and find :billing_products. You’ll see placeholder Stripe price IDs like price_1NLhPDIWVkWpNCp7trePDpmi. Replace each one with the real IDs from your Stripe account.

To list the prices for a product:

shell
stripe prices list --product=prod_xxxxx

If you’d rather not chase down IDs manually, ask Claude:

I've created Essential, Business, and Enterprise products in Stripe. Update
config.exs with the real price IDs by listing prices via the Stripe CLI.

Test it

Sign in to your app as the admin user (admin@example.com / password).

Navigate to Organizations in the sidebar. Click your org, then Subscribe. You’ll see your three plans with pricing.

Click Subscribe on a plan. You’ll be redirected to Stripe’s checkout. Use the test card 4242 4242 4242 4242 with any expiry and CVC. Submit.

You’ll be returned to the billing success page with your subscription active.

ℹ️ Information: If you see a spinner that never resolves, your Stripe webhook listener isn't running. Restart it with stripe listen --forward-to localhost:4000/webhooks/stripe.

To verify subscription-protected routes work, navigate to /app/org/:org_slug/subscribed_live — you should see a page that was previously blocked.

To add your own subscriber-only routes, edit lib/petal_pro_web/routes/subscription_routes.ex.

Org vs user subscriptions

Subscriptions can belong to organizations or individual users. The default is :org:

config/config.exs
# :org is the default. Set to :user to switch.
config :petal_pro, :billing_entity, :org

The relevant routes for org-based subscriptions:

/app/org/:org_slug/subscribe          # Pick a plan
/app/org/:org_slug/subscribe/success  # After purchase
/app/org/:org_slug/billing            # Manage subscription
/app/org/:org_slug/subscribed_live    # Subscriber-only example route

For user-based subscriptions:

/app/subscribe
/app/subscribe/success
/app/billing
/app/subscribed_live

In subscription_routes.ex there are two scope blocks — one for :user mode, one for :org mode. Add new subscriber-only routes inside the appropriate block.

Deploy to production

Three steps:

  1. Push the app to Fly.io
  2. Set up a webhook endpoint on Stripe pointing at your Fly app
  3. Set Stripe secrets on Fly.io

Push to Fly.io

Follow the Deploy to Fly.io guide.

Stripe webhook endpoint

In Stripe (test mode):

  1. DevelopersWebhooksAdd endpoint
  2. Endpoint URL: https://your-app-name.fly.dev/webhooks/stripe
  3. Under Select events to listen to, add:
    • customer.subscription.created
    • customer.subscription.updated
  4. Click Add endpoint

Fly.io secrets

You’ll need:

  • STRIPE_SECRET — from DevelopersAPI keysReveal test key
  • STRIPE_WEBHOOK_SECRET — from the Stripe webhook endpoint you just created (click Reveal under “Signing secret”)
shell
fly secrets set \
  STRIPE_SECRET="sk_test_xxx" \
  STRIPE_WEBHOOK_SECRET="whsec_xxx" \
  STRIPE_PRODUCTION_MODE="false"

After Fly redeploys, you should be able to subscribe through your live app using test cards.

Going live

Everything so far has been in test mode. To switch to production:

CLI commands in live mode

The Stripe CLI defaults to test mode. To work with live data, append --live:

shell
stripe products list --live

Not every command supports --live, but the ones in this guide do.

Recreate products in live mode

Repeat the Configure Stripe section in live mode. You’ll get new product and price IDs.

Before you update production config.exs, copy the existing test-mode billing config to config/dev.exs so your dev environment keeps working with test prices:

elixir
config :petal_pro, :billing_products, [
  %{
    id: "essential",
    name: "Essential",
    # ... your test-mode config
  }
]

Then update config.exs with the live-mode product/price IDs. Your dev machine uses test mode, your production server uses live mode.

Set STRIPE_PRODUCTION_MODE=true on Fly.io once you’re ready to take real payments.