The Product/Price Migration We’d Repeat (and What It Buys You Over a Billing Config File)

A code-level walkthrough of our product/price split, how it compares to MakerKit and Supastarter’s config catalogs, and why rows and pins hold up better once pricing, packaging, and entitlements all have to move.

FAQ

Do I need to redeploy to change pricing or packaging?

Not for most catalog work. Packages, scopes, limits, and trial flags are edited in Super Admin. Changing a list price still means a new prices row (Stripe immutability), not editing a number in place, and there isn’t a one-click “rotate price” flow in the UI yet.

How do you rotate prices in Refract?

Add a new prices row (new Stripe Price), promote it as the default, retire the old one. Pinned subscriptions.price_id keeps existing customers on the old rate. Product vs Price.

How is this different from MakerKit or Supastarter?

Both document a TypeScript payments config you redeploy. Neither enforces one active default price in the database. Grandfathering is usually a hidden flag on a product or plan in config, not a per-subscription row pin. Supastarter supports multiple currencies per plan in config; MakerKit’s examples often put currency on the product. We keep the catalog in Postgres with constraints and optional price_id pins. See the comparison table in the article (vendor docs change).

Can Refract do regional pricing or monthly vs annual on one plan?

The model supports sibling prices rows under one product (currency, interval, metadata on each row). We haven’t filled out a big multi-currency catalog yet; the insert path is what we shipped for.

Why is usage-based billing still one database row if Stripe has two prices?

Base Stripe price id on the row; metered id in prices.metadata.externalUsageId. Subscription creation expands to both. One commercial SKU in admin; we may split rows later if we need to grandfather base and meter separately.

Does Refract include A/B pricing or price experiments?

The schema can hold multiple live prices and pin cohorts via price_id. We have not shipped assignment, traffic splitting, or experiment analytics. That’s on you on top of the primitives.

How is this article different from “The Pricing Capabilities You'll Need Before You Think You Need Them”?

That one is when pricing capabilities matter. This one is what we built, how it compares to two common boilerplates, and which implementation choices we rejected.

If someone edits prices in the Stripe Dashboard, does Refract sync?

No catalog sync from Stripe product/price webhooks. Postgres is the catalog source of truth; webhooks reconcile subscription line items against ids we already know, including retired prices.