I Built the Pricing Knobs Before I Had Anyone to Turn Them

Brett Ridenour Brett Ridenour · June 13, 2026

Freebo’s first paying tenant is about to onboard. He runs a tour operation, he wants to use the platform, and somewhere in the conversation he asked the question every founder eventually hears: can we do better than the default?

The default platform fee on Freebo is 10%. He wanted 6%.

That kind of ask is supposed to be a moment. The dreaded “let me check with engineering.” The Slack thread. The migration. The Stripe Connect rate hardcoded in three places. The cron that already ran tonight at the old rate. The accounting team finding out next quarter.

Instead it was a single PUT request to staging. Thirty seconds.

I want to talk about why, because the answer is the whole reason I built Freebo the way I built it, and I think it’s the most underrated thing you can do early.

The first tenant is the test you don’t get to retake

If you build a SaaS with one customer in mind, that customer becomes the schema. Their currency. Their tax model. Their refund policy. Their fee structure. All of it leaks into your tables and routes as defaults that everyone else has to live with.

You don’t notice it until tenant two shows up and asks for something slightly different, and you discover that “the platform fee” isn’t a config — it’s a constant in quote-version.service.ts and three frontend components and a cron job nobody remembers.

I built Freebo with no tenants. That sounds like a disadvantage and it is, mostly. But it had one upside: I had nobody pulling the schema in a direction. So every knob got built as a knob.

What the knob actually looks like

Here’s the route that handles platform-fee configuration. It’s an unglamorous endpoint. I’d argue it’s the most important one I’ve shipped.

Platform fees PUT route

The PUT body accepts fee_type (percentage, fixed, or hybrid), a fee_value, optional min_fee / max_fee clamps, a fee_model (currently surcharge — the customer pays it on top of the booking total, not the operator), and a refund_fee_policy that controls what happens to the platform fee when a booking is refunded. Four policies: keep, proportional, full, and full_refund_only. The last one — only refund the platform fee on full refunds, keep it on partial ones — is the default, and it’s the kind of decision that’s a footgun if you don’t think about it before the first refund happens at 11pm on a Saturday.

To give my new tenant his 6%, I sent this:

Six percent surcharge

Done. The next quote his customers generate will compute platform_fee = subtotal_after_promo × 0.06 instead of × 0.10. Every downstream system — the pricing formula in quote-version.service.ts, the Formance ledger that posts the platform fee as a separate account entry, the Stripe Connect transfer that splits the money — picks the new rate up automatically, because nothing along that path ever knew what the “right” rate was supposed to be. They all read it from his row in the platform_fee_config table, scoped by location_id, like every other tenant-scoped thing in the system.

The boring discipline that makes this work

There are three rules I made myself follow when I was building the money parts of Freebo, and the 30-second 6% change is downstream of all three.

1. No constants. Anything a customer might one day negotiate is a row, not a literal. Platform fee, tax rate, refund policy, payout schedule, currency, even the rounding mode — all live in tables, scoped by location_id. The default is just a row you insert at signup. There’s a getDefaultFeeConfig() that exists to keep new locations from null-pointering, not to express truth about how the world works.

2. Quote versions are immutable. When a price changes, you don’t UPDATE — you INSERT a new quote_version row and point the reservation at it. The 6% change doesn’t retroactively rewrite his existing test quotes. Their math stays at 10% forever. New bookings get the new rate. This is the difference between “I changed the fee” and “I corrupted my financials.”

3. Money flows through Formance, not Postgres. Every fee change emits a ledger event — POST_RESERVATION_CHARGE, POST_QUOTE_DELTA, etc. — that ends up in a double-entry ledger as its own posting. Nothing about the platform fee lives as a balance in a Supabase column. If I want to know how much platform fee I’ve accrued from him this month, I ask the ledger, not a SUM().

Multi-tenancy is mostly the discipline of refusing to hardcode the first customer.

— The thing I keep relearning

Why I’m writing this down

I keep meeting founders who built single-tenant apps and are now trying to “add multi-tenancy.” It’s the worst migration in software. Every constant is a landmine. Every UPDATE you’ve ever shipped is a financial event you can’t reconstruct. Every cron is operating on someone’s data with someone else’s assumptions baked into it.

Doing it the other direction — building the knobs before you have anyone to turn them — costs you maybe 20% more time up front and saves you a rewrite later. And the first time a real customer asks for a real exception, you don’t sweat. You send a PUT.

That’s what I did this week. Six percent. Thirty seconds. The most boring deploy I’ve ever done.

And he’s not even live yet.