case studies

SaaS Plan Tiers With Bootstrapping: Install Prices Stay Put and Retired Plans Keep Their Customers

A product sold as a self-hosted install ships its plan tiers as data: Free, Team, Business, the seat limit of each, and which features each one turns on. The product team owns that list. But once an install is running, its owner sets prices for their own market, renames a tier to fit their sales pitch, and signs customers up to plans the product will later drop. The next release has to change the list without touching any of that.

The plans example in the repository plays this out. Release 1 ships three plans and three features. Then the install’s owner sets Team to 999 cents, renames Business to “Business Plus”, and signs a customer called Northwind up to the free plan. Release 2 drops the free plan, raises Team to 15 seats with a new list price of 1500, and adds an Enterprise tier and single sign-on.

The plan for release 2: insert the sso feature, update Team’s seats from 10 to 15, keep the admin’s name for Business, insert Enterprise, retire the free plan, and insert and delete feature links The plan for release 2, from bst plan, shortened to fit.

Three Columns, Three Owners

The seed file says who owns what. Price is marked local: the seed sets it when a plan is first inserted, and after that it belongs to the install. Release 2’s new list price of 1500 goes to new installs and never overwrites the 999 this owner chose, so the plan doesn’t even mention it. Seats are an ordinary merged column. Nobody had touched Team’s seat count, so the new 15 lands. The name is merged too, and here the owner had changed it, so “Business Plus” stays and the plan says so out loud as a kept edit.

table = "plans"
key = "code"
on_remove = { retire = "retired_at" }

[columns]
price_cents = "local"

Dropping a Plan People Still Pay For

Deleting the free plan would break Northwind’s subscription, which points at it by id. So the plans table uses on_remove = { retire = "retired_at" }: a plan dropped from the seed gets a timestamp instead of a delete. The application hides retired plans from the signup page, and the subscriptions that already use one keep working. The feature links are different. Those rows are pure reference data, so on_remove = "delete" removes the free plan’s link to export, and Enterprise gets its four links in the same transaction as the plan itself.

After the apply the example checks every value: Team at 999 cents and 15 seats, Business Plus, the free plan retired at the apply’s time, Northwind still on it, Enterprise with all four features, and a second plan that finds nothing left to do.

To run it, clone the repository and run go run ./examples/plans. The reference has every setting used here, and the other case studies cover country lists, workflow states and email templates.