Tier Hello World Demo
This is an example application that shows using Tier to integrate pricing, in a way that makes it possible to implement best PriceOps practices with a trivial amount of effort.
The example app is exceedingly simple, but the principles are flexible enough to easily be put into practice in much more complicated applications.
All of the code for this demo is available on GitHub, at tierrun/tier-node-demo.
The App
The application we'll be monetizing is a simple temperature conversion app. If you give it a Fahrenheit temperature, it'll convert it to Celsius, and vice versa. This is provided via a simple site built on express.
To see the app as it exists before adding any Tier integration,
check out the pre-tier
branch.
Nothing Up Our Sleeves
Nothing described here relies on any services running on https://tier.run, or anything at all other than Stripe.
You can think of Tier as a very fancy Stripe client that manages metadata and connections. It sets up your system so that the path of least resistance is also the path of optimum PriceOps.
Setting Up Tier
First, we'll have to install the Tier binary. On macOS machines, you can do this with Homebrew:
brew install tierrun/tap/tier
Binaries for major architectures can be found on GitHub.
You can also install it using go
version 1.19 or later:
go install tier.run/cmd/tier@latest
Once it's installed, use the tier connect
command to give Tier
access to your Stripe account. By default, Tier will only work
on test mode Stripe data, using a restricted key with permissions
that you can easily lock down.
Alternatively, you can set the STRIPE_API_KEY
in the
environment, if you have a key that you'd like Tier to use.
Installing Tier SDK
In the app, we install the Tier SDK by running:
npm install tier
Create Pricing Model
We create a pricing model by writing a pricing.json
file.
The pricing model is a simple free/pro scheme. Free accounts get 10 free temperature conversions per month, then they have to upgrade.
Pro accounts cost $10 per month, and get 100 conversions per month included with that base price. Beyond that, they will be charged $0.01 per conversion.
To do this, we define two plans in our pricing.json file with the
appropriate tiers. We're calling the feature feature:convert
.
{
"plans": {
"plan:free@1": {
"title": "Convert (free)",
"features": {
"feature:convert": {
"title": "Temperature Conversions",
"tiers": [
{
"upto": 10,
"price": 0
}
]
}
}
},
"plan:pro@1": {
"title": "Convert (Pro)",
"features": {
"feature:convert": {
"title": "Temperature Conversions",
"tiers": [
{
"base": 1000,
"price": 0,
"upto": 100
},
{
"price": 1
}
]
}
}
}
}
}
The most important part is that plans are named like
plan:<name>@<version>
, and features start with feature:
. But
if you try to do something against the rules, Tier will give you
an error telling you what's wrong.
When we want to change this scheme, we can add a new
plan (or a new version of the free
or pro
plan). Any
customers still on the old version will be unaffected.
To push the pricing model live, run:
tier push pricing.json
The /pricing
Page
In order to create a nice little two-column page showing the plan
options, we can pull the highest version of each plan with the
tier.pullLatest()
method, and hand that object off to our
template
to turn into HTML. I'm using EJS in this example, because it's
so dead simple to throw together an example like this, but you
can of course do the same thing with React, nunjucks, or any
other templating system.
Note: beware that this is definitely some demo magic. We're just sorting the plan versions lexically, but in practice, you'll probably want a config or some other system to say what the "public" version of any given plan is, so you don't end up with something like plan:free@zzzZZZ:final2.final.latest.final
. The tier.pullLatest()
method is marked as "experimental" in the Node SDK for this reason, we expect to add more utility in this area soon.
The important part is that we're not reading the file from disk, or hard-coding the plan details into our app. Instead, we pull from the single source of truth, and let that drive the rest of the system.
Subscribing Users To Plans
Stripe doesn't really have a concept of a "plan". There are Products which have Prices, and multiple Price objects can be attached to a customer's subscription.
Each of those Price objects has a unique identifier. So, if you want to treat multiple "Prices" as a "plan", you have to keep track of them to use the right ones when creating a subscription. As you add more tracked features, and test more different iterations of packaging them up into plans for customers, the complexity increases geometrically.
Thankfully, using Tier, we can just do:
await tier.subscribe(org, plan)
All of the Price objects associated with the plan will be attached automatically.
The org
is an opaque string that identifies the customer. It
must start with org:
, and it must be unique, but you can use
whatever identifier you use for customers in your system already.
These are all perfectly acceptable: org:user@email.com
,
org:beefcafebad1d3a
, org:213415-221321-4321
. There's
(almost) never any reason to deal with the Stripe Customer ID.
The plan
is the plan:<name>@<version>
from your
pricing.json
model. You should not hard code this! In the
demo, you'll note that we get it from a POST
request when the
user clicks the "Subscribe" button on the programmatically
generated pricing page.
No matter how many versions of your plans you have, the plan identifier is all you need to create the correct subscription for your customer.
Note: you can still create subscriptions using Tier that mix and match any prices and entitlements you like. We'll cover that in a future "advanced usage" blog post, as it's out of scope for a "Hello, World" app such as this.
Reporting Usage
Just like with subscriptions, rather than having to track Price objects
to know how to report feature usage to Stripe, using Tier, we just need
the org:...
identifier and the feature name from your pricing model.
await tier.report(org, "feature:convert")
The default count is 1, but if you want to report more than 1 of
something, you can just pass n
as the third argument:
await tier.report(org, "feature:morethanone", 100)
In this demo, we're reporting feature usage right at the point of delivery. For many use cases, that's perfectly fine. But, for example, if you're tracking download bandwidth or some other high-volume metric, you can of course roll that up and report it in a batch at any cadence that makes sense for your application.
The caveat, of course, is that the usage data you pull from Tier won't be fully up to date if you haven't yet updated it.
Limiting Access
We said that users on the free
account can only get 10
conversions per month. In order to make sure they haven't gone
over (and that they're on a plan that has access to the feature
at all!) we can call the tier.limit
method, like this:
const usage = await tier.limit(org, feature)
This method will return an object with used
and limit
fields,
which you can check to see whether the feature should be enabled.
Again, there's no need to keep track of Customer or Price objects, or even know what plan a user is subscribed to. Just check whether they have access to the feature, and if so, give them the feature.
Changing Plans
You can try out changing the pricing model any time you like, as often as you like:
tier push pricing-2.json
When you do this, the /pricing
page gets updated with the new
version of the plan, but importantly, the customer's plan isn't
changed. With Tier, grandparenting in your existing userbase is
the default, so you never have a situation where you try a
different price, and make everyone upset.
In fact, you could even have multiple versions of a plan living side by side, and see which one encourages better user behavior or gets better conversions.
Collecting Payment Info
For this, we still will need to go direct to Stripe, so that the
user can submit their credit card information directly to Stripe
from their browser, using stripe.Elements
.
Thankfully, the tier.whois(org)
method will give us their
Stripe Customer ID.
That's it!
In this demo, we took a working application and monetized it, without ever having to worry about managing Stripe object identifiers, and any future change to our pricing model is trivial.
There's a lot more documentation available on the Tier website. Try it out, and let us know what you think!