💳Adding a subscription
How to start charging customers in your web app
Starting with the Remindly app
If you haven't already, we recommend you complete the Remindly app guide before starting this one:
Creating a web app from start to finishFollow a step by step guide to creating a reminders web application using the Petal Pro boilerplate. We will cover everything from setup to deploying to production.If you do, skip to the Configure Stripe section below. Alternatively, you can keep reading to create a new application.
Starting with a fresh app
For this tutorial you will need:
Access to Petal Pro (you can purchase it on petal.build)
Elixir & Erlang installed (you can follow our installation guide to get up and running)
PostgreSQL running: Postgres.app works well for Mac or we have a docker-compose.yaml file that will run Postgres for you
An account on Stripe
For deployment (optional) you will need:
An account on Fly.io
Download
Head over to the projects page and create a new project, then download v1.7.0. You can unzip it in your terminal with the command unzip petal_pro_1.7.0.zip
.
Run
First, rename the project:
mv petal_pro_1.7.0 sub_demo
cd sub_demo
mix rename PetalPro SubDemo
Next, start the demo app:
mix setup
mix phx.server
Configure Stripe
Before we do anything inside the demo app, we need to add some Stripe Products. Login using your account. Make sure that Stripe is in "test mode" - this will allow you to test purchasing without a credit card.
Install the Stripe CLI
Follow these directions to install the Stripe CLI. Then login via the command line (the instructions to login are also on this page):
stripe login
Create the "Essential" product
First create the "Essential" product:
stripe products create --name="Essential"
Take note of the id
returned, it will look like prod_xxxxx
.
Create the Monthly price:
stripe prices create \
--unit-amount=1900 \
--currency=usd \
-d "recurring[interval]"=month \
--product="prod_xxxxx"
Make sure you replace prod_xxxxx
with the id
returned from the previous call.
Now do the same thing for the Yearly price:
stripe prices create \
--unit-amount=19900 \
--currency=usd \
-d "recurring[interval]"=year \
--product="prod_xxxxx"
Add the "Business" and "Enterprise" products
Repeat the process (above) to create a "Business" and an "Enterprise" product. Use the table below as a point of reference for suggested prices:
Monthly price
$19
$49
$99
Yearly price
$199
$499
$999
Enable switching plans for the Customer Portal
When purchasing a subscription, Petal Pro will redirect to Stripe. In the case where the user switches from an existing plan to another - you need to enable some settings for the Customer Portal. Once logged into Stripe:
Make sure Stripe is in "Test mode"
Click on the cog on the top right, then click on
Settings
In the section titled "Billing", click on
Customer Portal
Expand the "Subscriptions" section
Enable the setting,
Customers can switch plans
Using "Find a test product..." add prices from the Essential, Business and Enterprise products
In addition, you may want to review the "Business information" section - to customise the output of the Customer Portal
Configure Petal Pro
Petal Pro comes pre-configured for Stripe integration. There are three things you need to do to test the integration locally:
Run the CLI web hook
Add Stripe settings to your development environment
Update the Petal Pro config with product/price details (created above) from your Stripe account
Run the CLI web hook
In order to test your Stripe integration on your dev machine, you'll need to run a web hook. This is done using the Stripe CLI (see the Install the Stripe CLI section for more information).
Once you are logged in via the Stripe CLI, you can run the following command:
stripe listen --forward-to localhost:4000/webhooks/stripe
> Ready! You are using Stripe API Version [2022-11-15]. Your webhook signing secret is whsec_xxxxxxxxxxxxxxx (^C to quit)
Take note of the whsec_xxxxxxxxxxxxxxx
secret. This will be used as your STRIPE_WEBHOOK_SECRET
in the next section.
Add Stripe settings to your development environment
First, you'll need your STRIPE_SECRET
and your STRIPE_WEBHOOK_SECRET
. To get your STRIPE_SECRET
:
Go to the Stripe console
Make sure you're in "Test mode"
At the top of the screen, click on
Developers
then clickAPI keys
Under "Standard Keys", look for the "Secret key"
Click
Reveal test key
Click the key to copy it
To get the STRIPE_WEBHOOK_SECRET
, see the instructions under Run the CLI web hook.
The next step is to add these as environment variables to your demo app. Make sure that you install direnv and that you're in the root of the demo project:
cp .envrc.example .envrc
Uncomment these lines at the bottom of the file:
# If using Stripe for payments:
export STRIPE_SECRET=""
export STRIPE_WEBHOOK_SECRET=""
export STRIPE_PRODUCTION_MODE="false"
Update the values you obtained (above) for STRIPE_SECRET
and STRIPE_WEBHOOK_SECRET
.
The .envrc
file is listed in .gitignore
. Meaning that the secrets will live on your development machine and will not be accidentally pushed to the git
repo.
To activate the new environment variables:
direnv allow
Update the Petal Pro config
Petal Pro is shipped with the following config:
config :petal_pro, :billing_products, [
%{
id: "essential",
name: "Essential",
description: "Essential description",
most_popular: true,
features: [
"Essential feature 1",
"Essential feature 2",
"Essential feature 3"
],
plans: [
%{
id: "essential-monthly",
name: "Monthly",
amount: 1900,
interval: :month,
allow_promotion_codes: true,
trial_days: 7,
items: [
%{price: "price_1NLhPDIWVkWpNCp7trePDpmi", quantity: 1}
]
},
%{
id: "essential-yearly",
name: "Yearly",
amount: 19_900,
interval: :year,
allow_promotion_codes: true,
items: [
%{price: "price_1NWBYjIWVkWpNCp7pw4GpjI6", quantity: 1}
]
}
]
},
# Business and Enterprise config too
}
Note that the essential-monthly
and essential-yearly
plans contain prices that refer to Stripe (e.g. price_1NLhPDIWVkWpNCp7trePDpmi
). These prices need to be replaced with Stripe Price ids that you generated.
To get the list of prices for the "Essential" product:
# To get the "Essential" Product id
stripe products search \
--query="name:'Essential'"
# To list all Prices that belong to the "Essential" product
stripe prices list \
--product="essential_product_id" \
--active=true
In the config, replace the essential-monthly
and essential-yearly
item/price with the ids returned from the Stripe CLI. Repeat the above process for the Business and Enterprise plans.
Test the demo
In the web app, login as the admin user.
Navigate to the Organisations page:
http://localhost:4000/app/orgs
Click on your Organisation, then click "Subscribe". You'll see the following page:

Choose a plan by clicking a Subscribe
button. You'll be redirected to the Stripe checkout session:

Because Stripe is in test mode, you can use the test card 4242 4242 4242 4242
. Fill out the details and hit the Subscribe
button.
Once the purchase is successful, you'll be returned to the billing success page:

Now that the subscription is active, the following route becomes accessible:
http://localhost:4000/app/subscribed_live

If you want to add another route that is only accessible to subscribers you can do this in lib/petal_pro_web/routes/subscription_routes.ex
.
Org vs User subscriptions
Subscriptions either belong to Organisations or individual Users. The default behaviour is set to Organisations:
# :org is the default. Set to :user to change this setting
config :petal_pro, :billing_entity, :org
For org-based subscriptions, the following routes are relevant:
# Subscribe - admin users can select a plan to purchase
/app/org/:org_slug/subscribe
/app/org/:org_slug/subscribe/success
# Billing - admin users can see the existing plan and choose to cancel
/app/org/:org_slug/billing
# Access for users that belong to a subscribed organisation
/app/org/:org_slug/subscribed_live
There is an equivalent set of routes for user-based subscriptions:
# Subscribe - users can select a plan to purchase
/app/subscribe
/app/subscribe/success
# Billing - users can see the existing plan and choose to cancel
/app/billing
# Access for subscribed users
/app/subscribed_live
Finally, the last thing to be aware of is lib/petal_pro_web/routes/subscription_routes.ex
. There are two locations in this file where you can add new routes for subscribed users:
scope "/app", PetalProWeb do
pipe_through [:browser, :authenticated, :subscribed_user]
# New routes for :user mode belong here
end
scope "/app", PetalProWeb do
pipe_through [:browser, :authenticated, :subscribed_org]
# New routes for :org mode belong here
end
Deployment with Fly.io
Once your demo is ready, you can push it to production. To do so, there's three steps:
Push the demo app to Fly.io
Setup webhook endpoint on Stripe
Configure Fly.io environment variables
Push the demo app to Fly.io
We have found Fly.io to be the best combination of cheap and easy. Petal Pro has been set up for users to quickly deploy on Fly.io's servers.
If you haven't already, download the Fly.io CLI. Then you will need to register or sign in.
Once signed in, you can create a new project with:
fly launch
We'll call the app
sub-demo
.Hit
Y
to setting up a DB as we'll need that.Go with the cheapest option for a server
Hit
N
to Upstash RedisWhen it asks "Do you want to deploy now", hit
N
- we need to make a change before we deploy.
Email sending
To be able to register/sign in, we'll need to ensure email is set up and we'll need a service to send our emails out. We've found that the simplest and cheapest solution is Amazon SES, and so Petal defaults to using this. Look in runtime.exs
to see the setup:
config :remindly, Remindly.Mailer,
adapter: Swoosh.Adapters.AmazonSES,
region: System.get_env("AWS_REGION"),
access_key: System.get_env("AWS_ACCESS_KEY"),
secret: System.get_env("AWS_SECRET")
We don't really use Amazon for much else, but its email service is cheap and the emails don't get sent to spam as easily as other services we've tried (cough cough Sendgrid).
Setting up Amazon SES is beyond the scope of this tutorial. You can read their docs here to set it up. The end result should be you are able to provide the following secrets that we'll provide to our production server:
fly secrets set AWS_ACCESS_KEY="xxx" AWS_SECRET="xxx" AWS_REGION="xxx"
If you don't want to use SES you can switch to a different Swoosh adapter.
Telling Fly to add our "petal" repo
Since Petal Framework is not coming from hex.pm, we need Fly to know to add our Petal registry.
After running the fly launch
command above, Fly has generated a dockerfile in the root of our project.
Open Dockerfile
and search for this bit:
# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config
Before that mix deps.get
command is run, we need to add the Petal repo. So, above this block of code you want to add:
RUN --mount=type=secret,id=PETAL_LICENSE_KEY \
mix hex.repo add petal https://petal.build/repo \
--fetch-public-key "SHA256:6Ff7LeQCh4464psGV3w4a8WxReEwRl+xWmgtuHdHsjs" \
--auth-key "$(cat /run/secrets/PETAL_LICENSE_KEY)"
Finally, we can run fly deploy --build-secret PETAL_LICENSE_KEY=<your key>
You can see your key in the install instructions on Petal (see Step 1 and copy the key written after "--auth-key").
After deploying you can run fly open
to see it in your browser. But we're not done yet!
Setup webhook endpoint on Stripe
You can use the Stripe CLI to run the webhook when working on your own dev machine. However, for a hosted service, you need to configure an endpoint in the Stripe console:
Open the Stripe console
Make sure you're in "Test mode"
Click on
Developers
at the top of the pageThen click on
Webhooks
Click on
Add endpoint
For the Endpoint url
enter the following (replace your-host-name
with the name of your fly application):
https://your-host-name.fly.dev/webhooks/stripe
Further down the page under the heading "Select events to listen to" - click on + Select events
. Add the following events to the list:
customer.subscription.created
; andcustomer.subscription.updated
Finally, click the Add endpoint
button.
Configure Fly.io environment variables
The demo app running in Fly.io needs to be configured for Stripe. We need two settings. First, to obtain the STRIPE_SECRET
:
In Stripe, make sure you're in "Test mode"
Click on
Developers
Click on
API Keys
Click on
Reveal test key
Click the key to copy it. This is for our
STRIPE_SECRET
environment variable
Next, to obtain the STRIPE_WEBHOOK_SECRET
:
In
Developers
click onWebhooks
Click on the endpoint you just created
Under "Signing secret" click on
Reveal
Copy the secret
Now at the command line, run the following command:
fly secrets set _
STRIPE_SECRET="sk_test_xxx" _
STRIPE_WEBHOOK_SECRET="whsec_xxx" _
STRIPE_PRODUCTION_MODE="false"
Once Fly has finished deploying you're new secrets, you should be able to purchase a subscription using the Fly demo app!
From test mode to production
One last thing. All Stripe configuration (up until this point) has been setup exclusively in "Test mode". The main focus of this tutorial is to get you up and running - without worrying about payments with real credit cards.
To prepare your app for production, you can follow this tutorial with "Live mode". Before you do, please read the next two sections.
Using the CLI with "Live mode"
The Stripe CLI defaults to "Test mode". To work in "Live mode" you'll need to pass in an extra parameter. For example, if you wanted to list products in live mode you would do the following:
stripe products list --live
Not all Stripe CLI commands support the --live
flag. But it will work with the commands listed in this tutorial.
Recreate the Stripe Products
You'll need to repeat the Configure stripe section - re-creating the products in "Live mode". This means that you'll end up creating the same products, but they will have different ids. Before you update the configuration in /config/config.exs
, I suggest that you copy the following section to /config/dev.exs
:
config :petal_pro, :billing_products, [
%{
id: "essential",
name: "Essential",
description: "Essential description",
most_popular: true,
# ...
}
That way you can stick with "Test mode" on your dev machine and then use /config/config.exs
for your production server.
Was this helpful?