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:
- A Stripe account
- The Stripe CLI installed locally
- An account on Fly.io if you want to deploy
ℹ️ 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:
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:
stripe products create --name="Essential"
Copy the id from the response — it looks like prod_xxxxx.
Create the monthly price:
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:
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:
- In Stripe (test mode), click the cog (top right) → Settings
- In Billing, click Customer Portal
- Expand Subscriptions
- Enable Customers can switch plans
- 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:
- Run the Stripe webhook listener
- Add Stripe secrets to your environment
- Update the Petal Pro config with your product/price IDs
Webhook listener
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 → Developers → API keys → Reveal test key -
STRIPE_WEBHOOK_SECRET— from thestripe listenoutput above
Petal Pro uses direnv for local environment variables. Install it, then:
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:
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:
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:
# :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:
- Push the app to Fly.io
- Set up a webhook endpoint on Stripe pointing at your Fly app
- Set Stripe secrets on Fly.io
Push to Fly.io
Follow the Deploy to Fly.io guide.
Stripe webhook endpoint
In Stripe (test mode):
- Developers → Webhooks → Add endpoint
-
Endpoint URL:
https://your-app-name.fly.dev/webhooks/stripe -
Under Select events to listen to, add:
-
customer.subscription.created -
customer.subscription.updated
-
- Click Add endpoint
Fly.io secrets
You’ll need:
-
STRIPE_SECRET— from Developers → API keys → Reveal test key -
STRIPE_WEBHOOK_SECRET— from the Stripe webhook endpoint you just created (click Reveal under “Signing secret”)
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:
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:
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.