Deep Dive

This section covers the architecture of the Tier system in greater detail.

If you're just getting started, check out Getting Started and Tier Basic Concepts first.

Table of Contents

What is Tier?

Tier is a tool to help you get started charging for your application quickly and easily, while also making it possible for you to change your pricing model easily and safely in the future.

Tier is pricing as code.

The Problem

You came up with a brilliant idea, created a proof of concept, and shopped it around to investors. They saw how great your idea was, and put their money behind it.

Congratulations!

Now, you have to hurry up and start charging, before you run out of money. Also, the proof of concept needs a ton of work to be production ready.

So you slap together some plans, and spend as little time as you can creating a paid plan in your application. Copy and paste from a Stripe tutorial you found. Hard code the plan names right in there, it's fine, we just have to get something working.

We'll fix it later.

Later comes...

You realize that you're charging in a way that really doesn't properly match the value that your customers get from your app.

A new competitor shows up, with a pricing model that completely shifts the landscape.

Some users are complaining that they're being over charged, so you hand out discounts to keep them happy. Other customers are quietly using up all your resources and only paying a fraction of what they're costing you.

You know you need to change your pricing model, but your engineering team is hesitant to touch that code, since it is tied into everything. You'll have to figure out how to handle customers who signed up on the old plan, how to keep your accounting in order, the problems are endless. You're stuck.

How Does Tier Help

Tier provides a way to define your pricing model that is easier to get started, and safe to change.

Easier to Start

We streamline all of the steps of getting set up with Stripe, defining Prices, Customers, Plans, Subscriptions, and so on.

You define your pricing model as a configuration file that specifies the features and tiers available to each plan.

When a user signs up for your application, you assign them to a plan, using whatever unique customer identifier your application is already using. Any time they upgrade or downgrade their plan, you call that same function to append a new phase to their schedule.

Within your application, wrap your features in Tier function calls to check entitlement and report usage. If the user doesn't have access to a paid feature, Tier will tell you. If they use a feature, Tier will track it for you.

At the end of the month, Tier will invoice the customer.

And if you're not sure what you should be billing for, that's fine! Just wrap everything in Tier checks, mark the features as free/unlimited, and figure it out later based on actual data.

Safe to Change

Whenever you want to release a new version of a plan, define it in your pricing model, and push it live.

Customers who signed up with the old plan will stay on their old plan (unless you switch them to the new version of the plan, of course).

New customers who sign up will use the new version of the plan.

Each customer will be charged according to the plan that they have active and the usage that they incur under that plan.

In other words, you can try out new versions of your pricing model at any time, as often as you like, with no code changes, and no disruption to your application or your business.

You will not get stuck, and don't have to worry about getting your pricing exactly right on the first try.

Why This Is Important

Products that iterate on their pricing are more likely to succeed.

Traditionally changes to pricing and packaging have required an enormous amount of very careful work. It connects to every part of your business, and anything that goes wrong can spell disaster, so you'd better get it right. It's a high-risk, low-information.

Tier says: what if, instead of being a big expensive dangerous project, changing your pricing model was free and easy and safe? What if you could just test out any pricing idea you wanted?

So go ahead and get it wrong at first. It's fine. Iterate until you get it right.

Model

The "model" is a JSON representation of the plans you define for your application. Each plan contains a set of features, with the tiers defined for usage within that plan. If a plan doesn't include a feature, then that means users on that plan don't have access to that feature.

{
  "plans": {
    "plan:free@1": {
      "features": {
        "feature:song-stream": {
          "tiers": [
            // $1 each stream, but capped at 100 streams/month
            { "price": 100, "upto": 100 }
            // no tiers after this one means no more usage
          ]
        }
      }
    },
    "plan:pro@1": {
      "features": {
        "feature:song-stream": {
          "tiers": [
            // first 200 cost $0.50 each, but $10 up front
            { "price": 50, "upto": 200, "base": 1000 },
            // streams 201-1000 cost $0.10 each
            { "price": 10, "upto": 1000 },
            // then they're free
            {},
          ]
        },
        "feature:song-download": {
          "tiers": [
            // using downloads at all costs $10, but
            // no charge other than the base price
            { "base": 1000 }
          ]
        }
      }
    }
  }
}

Plans

Every billing plan that a user might be signed up for is identified with a string starting with 'plan:', and containing a @. The part before the @ is the "name". The part after the @ is the "version".

You can define as many versions of as many plans as you want in your pricing model. However, you may not change a given version of a plan, so to make changes you will create a new version of it. This makes it possible to experiment safely with pricing changes, and only update existing customers' plans when it makes sense for your application.

Think of your set of plans as an append-only set of the various ways you package your application and bill your customers.

Append-Only

The goal of Tier is to enable iteration on your pricing structure, in a way that is easy, reliable, and safe.

One of the hardest pieces of any pricing change in a SaaS product is dealing with users who signed up for legacy pricing plans. If their price goes up, you may need to apply discounts to all legacy users to keep them happy, or add custom logic throughout your application to continue to give them access to features they've paid for.

If a certain user negotiates a special price for some reason (special volume discounts for large enterprise customers, for example), then this gets even more complicated, and the risk of losing the customer can be even greater.

Each plan in the Tier model has a name and a version. In order to prevent changing the behavior of an existing user's plan, Tier will not allow you to modify a plan version once it exists. However, if you add a new version of a plan, then that can become the default for new users signing up for your service.

Features

Every feature in your application is identified with a string starting with feature:. The rest of the string can contain any arbitrary identifier that you use internally to reference a feature. You can think of this as a feature flag for restricting usage to paid features and tracking how much of something a customer has consumed.

The feature name should identify what thing the user is trying to do or consume, which you might bill for (or at least track). For example:

The value of a feature object in the model can have the following fields:

Tiers

Each feature has a set of 0 or more "tiers", which define the prices and limits for use of that feature. Each tier has the following fields:

The simplest "free, unlimited" tier is {}. Since it doesn't specify anything, it uses the defaults: up to Infinity, $0.00 per unit, $0.00 base price.

For example, if we want to say that streaming a song on our platform costs $1 each for the first 100, and then $0.50 thereafter, we could do:

{
  "plans": {
    "plan:streamer@123": {
      "features": {
        "feature:song-stream": {
          "tiers": [
            // $1 for the first 100
            { "price": 100, "upto": 100 },
            // $0.50 thereafter
            { "price": 50 }
          ]
        }
      }
    }
  }
}

A feature with no tiers means that users on the plan are not entitled to that feature.

{
  "plans": {
    "plan:streamer@123": {
      "features": {
        "feature:song-stream": {
          "tiers": [{ "price": 100, "upto": 100 }, { "price": 50 }]
        },
        // Explicitly disabled. Any usage will be treated as overage.
        "features:song-download": {
          "tiers": []
        }
      }
    }
  }
}

Pushing a Model

Create your model in any filename that you prefer. We typically use pricing.json.

Push the pricing model from the command line by installing one of the Tier SDKs and then running:

tier push pricing.json

In the background, this is just making a request to the Tier API. You can also send it programmatically by using the tier.pushModel(<filename>) function in the SDK, or by making an http request using your tool of choice:

curl -X POST \
  -u $TIER_KEY: \
  -H "Content-Type: application/json" \
  -d $(cat pricing.json)

Pricing Pages

Note: this feature is experimental, and under active development! Please try it out, but expect changes and give us feedback about what works for you, what you'd like to see, etc.

As you define plans in your pricing model, of course it does not make things much easier unless the users in your application can be signed up for the latest and greatest versions of each plan.

Your app needs a way to programmatically get the list of plans that should be shown to users, so that the registration flow can be driven entirely by your pricing model definition, rather than hard-coded plan names.

This is where Pricing Pages come in.

Pricing Page Data Model

The Pricing Page is a subset of the plans defined in your model.

Each pricing page data object contains the following fields:

Fetching a Pricing Page

You can fetch a pricing page by using the Tier SDK, or making a request to https://app.tier.run/web/pricing-page/-/${name}. (Omit the ${name} for the default pricing page.)

Turning Pricing Page into HTML

The data provided by Tier can be transformed into HTML using your templating language of choice, however your application does such things.

See the tier-node-demo for an example of this.

Signing Up New Users, Adjusting Plans, etc.

If you use the Tier pricing page as your source of available plans, then you can easily pass the selected plan name to your application logic.

There, your application can call tier.appendPhase(org, plan) without having to hard-code anything.

The result is fully data-driven pricing plan management, where any change to your model will update the options presented to a user, without additional code changes.

Orgs

Every customer of your application is identified to tier by a string starting with 'org:'. The rest of the string can contain any arbitrary identifier that you use internally to reference the customer.

These are all valid OrgName values:

The important thing is that you can link the org to an actual user within your app, so that you always send the same OrgName for the same actual customer.

Org Schedule

Each org in the system has a schedule which determines the plan that covers that org's usage, in the past and future.

For example:

$ tier fetch /api/v1/schedule?org=org:acme

{
  "phases": [
    {
      "org": "org:acme",
      "plan": "plan:start@0",
      "scheduled": "2022-06-15T10:36:38.979021-07:00",
      "effective": "2022-06-15T10:36:38.958-07:00"
    },
    {
      "org": "org:acme",
      "plan": "plan:paid@24",
      "scheduled": "2022-06-19T10:36:38.979021-07:00",
      "effective": "2022-06-19T10:36:38.958-07:00"
    }
  ]
}

Here we see two phases. The first, plan:start@0 started on 2022-06-15. Then, 4 days later, they upgrade to plan:paid@24.

You can do this programmatically in the SDK by using tier.lookupSchedule(org).

Appending Phases to Org Schedule

To append a phase to an org's schedule, use the tier.appendPhase(org:OrgName, plan:PlanName, effective?:Date) method.

To make the new phase effective immediately, simply omit the effective parameter.

The org parameter needs to be a valid org name (ie, a string starting with "org:") which you will use throughout the application to report usage for this org.

The plan parameter needs to be a valid plan identifier with both name and version, as defined in your pricing model.

You will typically want to create an initial "free" or "start" plan on user sign-up just to record that the org exists. When and if the user upgrades or changes their plan, call tier.appendPhase() again to note the change.

Example: Trials

You can use the effective date to set a user on a trial for some period of time, and then transition them to a paid plan when their trial expires. For example:

const 1day = 1000 * 60 * 60 * 24
const trialLength = 1day * 14 // 2 weeks

// here "account" is an object that we got from our
// database representing the account.  The "id" field is
// the unique identifier we use for the tier org.
//
// the trialPlan and paidPlan arguments come in from our
// pricing page definition.
export function paidTrial (account, trialPlan, paidPlan) {
  // put them on the free trial plan right away
  await tier.appendPhase(`org:${account.id}`, trialPlan)
  // transition to paid plan after the trial ends
  const trialEnd = new Date(Date.now() + trialLength)
  await tier.appendPhase(`org:${account.id}`, paidPlan, trialEnd)
  // however you schedule emails is up to you.
  scheduleYourTrialIsEndingEmail(account, trialEnd)
}

Look Up Org Details

You can get the details about an org using the SDK by calling tier.lookupOrg(org). This will give you information about the org's billing details, if they have been set.

For example:

{
  "id": "org:asdf1234foobar",
  "name": "org:asdf1234foobar",
  "default_payment_method": {
    "billing_details": {
      "address": {
        "country": "US",
        "postal_code": "42424"
      }
    },
    "card": {
      "brand": "visa",
      "exp_month": 4,
      "exp_year": 2024,
      "last4": "4242"
    }
  },
  "live_mode": true,
  "url": "https:/dashboard.stripe.com/test/connect/accounts/acct_9L6YuJ2CPREEhBW0/customers/cus_DBwcNBiLsLeAKt"
}

Included fields:

Reporting Usage

Feature usage can be checked by calling tierClient.can(), and reported using tierClient.report().

In general, if can() returns false, then the user is over the limit or doesn't have access to the feature in their plan, so you should not give them the feature.

report() can be called right away, ahead of delivering the feature, or at any arbitrary time.

For example:

if (await tier.can('org:acme', 'feature:foo')) {
  consumeOneFoo('acme')
  await tier.report('org:acme', 'feature:foo')
} else {
  // suggest they buy a bigger plan, maybe?
  showUpgradePlanUX('acme')
}

Counters

Tier of course has no idea what your actual features are, really. You could be shipping stamps, serving files, washing cars, anything.

Tier is, at its core, a series of timers and counters that your app uses to track the usage of whatever it is your app does.

Internally, there is a positive counter, and a negative counter, so that Tier can track each usage increase and decrease over time.

Overages

When usage puts an org beyond the last tier in their plan for that feature, they go into an "overage" state.

Overages are not billed, but they are reported.

If a feature isn't delivered, you can tier.report() with a negative number to roll it back.

Timing

Usage is counted against the phase in the org schedule corresponding to their effective date.

This "tallying up" of usage occurs at the end of the billing phase. Note that report() and appendPhase() both take a Date argument indicating when they are effective. (In both cases, the effective date defaults to "right now".)

More Usage Examples

See the SDK documentation for more examples of using the Tier client to decide who gets access to your app's features.