draft

vim ./src/drafts/tier-simple-demo.md

Tier Hello World

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.


🙈 All Stripe API calls go directly to Stripe, not through Tier.


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

f32b5b4

Create Pricing Model

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.

The easiest way to create this pricing model is to use the Tier Plan Builder. Feel free to play around with the builder yourself, or you can pull the finished product.

Tier can fetch the model from the builder, so this works:

tier push https://model.tier.run/cld3ccfs80ifyftf4i74vnu3z

Alternatively, if you enjoy writing JSON by hand, or just want to track the file in source control or do other things with it, you can take a look at the pricing.json file we're using. Documentation for the format is available in the Tier docs.

3f6e7cf

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.

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.

c0a7859

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" and treat them 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 a plan for customers, the complexity increases geometrically.

Thankfully, using Tier, we can just do:

await tier.subscribe(org, plan)

All of the Price objects that it created 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 name and version 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.

ecb1cf1

Using Stripe Checkout

Using tier.subscribe() is fine on its own, but you might want to collect your customer's information using Stripe Checkout.

To do that with Tier, we provide the success_url and cancel_url to tier.subscribe(), and redirect the user to the resulting url field.

In the demo app, we can use the actual-request-url module to figure the url we're coming from, and redirect them appropriately.

const reqUrl = actualRequestUrl(req)
const successUrl = String(new URL('/checkout_success?plan='+plan, reqUrl))
const cancelUrl = String(new URL('/checkout_cancel', reqUrl))

const { url } = await tier.checkout(`org:${user}`, successUrl, {
  cancelUrl,
  features: plan,
})
res.redirect(url)

Once they complete the Stripe Checkout process, they'll be subscribed to the specified plan.

75eb851

Reporting Usage

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.

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.

18bfb50

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.

920f8c1

Changing Plans

You can try out changing the pricing model any time you like, as often as you like:

tier push pricing-2.json

Or, if you make edits to the pricing model in the builder, you can run tier push with the URL again.

For demo purposes, we've cloned it to another pricing.json document, so you can get the v2 of the demo model here:

tier push https://model.tier.run/cld3cjaji0ijpftf4v116atth

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

Normally, you'd have to use Stripe's SetupIntents API with stripe.Elements to create a credit card collection form on your site in a way that's PCI compliant.

Tier does make that pretty straightforward, by providing the tier.whois(org) method to get the Stripe Customer ID.

However, using Checkout, we can do it even easier, with a whole lot less code. Instead of collecting the payment information ourselves, we'll use tier.checkout() without providing a plan, to get a URL where the user can update their Customer information without changing any subscriptions as a result.

routes.get('/payment', log, mustLogin, async (req, res) => {
  const user = req.cookies.user
  const reqUrl = actualRequestUrl(req)
  const success = String(new URL('/', reqUrl))
  const { url } = await tier.checkout(`org:${user}`, success)
  res.redirect(url)
})

75eb851

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.