There is a pricing engine in Quotify. It lives in calculate-price.ts, it is tested, and it works. So naturally, the first thing I did when we started selling downloadable quote templates was write the whole thing again in Excel formulas.
That sounds like a mistake. Every instinct I have says don’t implement the same logic twice, because the two copies will drift and you’ll spend a Tuesday afternoon working out which one is lying. But it was the right call, and the reasoning is worth unpacking.
People wanted the thing, not the account
Quotify has 66 quote templates: painter and decorator, plasterer, dog groomer, driving instructor, the lot. They’re forms. You fill one in, it prices the job, you send it.
The trouble is that some people don’t want a form. They want a spreadsheet they can open, poke at, and keep on their own machine without signing up to anything. That’s a perfectly reasonable thing to want, and telling those people “actually you should change how you work” is a great way to sell nothing.
So the templates are now downloadable as .xlsx packs. Seven quid each, no account needed.
But a static spreadsheet with a price list in it is worth roughly nothing. The value is that it works out the total for you, the same way the app does. Which means the maths has to come along too.
Generating 66 spreadsheets you’d actually want to use
Every template already has a form_schema in Supabase. It knows the questions, the pricing model behind each one, and the units. All the information needed to build a working spreadsheet is sitting there already, so the packs are generated from it rather than made by hand.
A build script reads each schema and produces a self-calculating workbook with ExcelJS. The formulas mirror the app: flat option prices, quantity times unit rate, area as width times height, perimeter as width plus height times two. Then the whole base total gets multiplied by whatever percentage surcharges you’ve ticked.
Each pack has three tabs. A Calculator where you actually work the job out, a Your Quote tab that’s branded and exports to a tidy A4 PDF, and a How to use tab, because a spreadsheet with no instructions is a support ticket with extra steps.
One deliberate omission: the app’s conditional logic is flattened. In the web version, questions appear and disappear based on your answers. In a spreadsheet that would mean hiding rows with macros, and macros mean security warnings, and security warnings mean people email you instead of using the thing. So every question is just there. Slightly longer, considerably less annoying.
There’s a fussy little detail I’m unreasonably pleased with. Column A and column F are narrow empty margin columns, so the content has breathing room from the edge of the page instead of being jammed against it. It’s the spreadsheet equivalent of padding, and it’s the difference between “generated file” and “something someone made”.
So about that duplication
Yes, the maths now exists in two places. TypeScript in the app, formulas in the workbook.
I could have avoided it. I could have generated the spreadsheets by running the real engine server-side and baking in static numbers. But then you’d have a dead spreadsheet, and the entire point is that you can change a quantity and watch the total move. The formulas have to be real formulas.
So it’s duplication, on purpose, with eyes open. The mitigation is that the pricing model is genuinely small, the generator is one script, and both live in the same repo where changing one and forgetting the other is at least visible. That’s not elegant. It’s just honest about the trade.
Paddle, and 66 prices I didn’t want to create by hand
Selling a digital file to whoever turns up means dealing with VAT in whatever country they’re in. Paddle acts as merchant of record, which means that becomes their problem rather than mine. That’s the whole reason it’s in the stack.
The setup is one product with 66 prices hanging off it, one per template. Creating those through a dashboard 66 times was never happening, so there’s a sync script.
The bit worth stealing is how it stays idempotent. Every price gets tagged with its template slug in Paddle’s custom_data. When the script runs it reads back what already exists, matches on that tag, and only creates prices for templates that don’t have one. Add four new templates, run it again, get four new prices and no duplicates. It writes the resulting price ids back into a manifest that’s committed to the repo, so the site always knows what to charge without calling an API at build time.
If a template has no price id for the current environment, the buy card renders in a disabled state rather than exploding. Nothing to sell yet is a valid condition.
The database I didn’t build
Here’s the part I’d defend in a code review.
When someone buys, Paddle fires a transaction.completed webhook. That handler verifies the signature against the raw body first, because fulfilling an unverified request is how you end up giving your product away. Then it mints a signed, time-limited download link and emails it over.
What it does not do is write a row anywhere.
The template slug travels through the checkout in customData, so the webhook already knows what was bought without looking anything up. And the download token is HMAC-signed and carries the slug and expiry inside it. A valid signature is the grant. There’s no orders table, no purchases table, nothing to migrate, nothing to back up, nothing to leak.
If someone loses the email, a separate endpoint asks Paddle for their transactions, re-mints fresh links, and sends them again. Paddle already holds the order history, so keeping a second copy of it would just be a synchronisation bug waiting to happen.
That’s the bit I’d encourage you to nick. Before you add a table, check whether something upstream already knows the answer.
Where it landed
66 generated spreadsheets, one product, 66 prices, a webhook, and no persistence layer. The maths lives in two places and I’ve made my peace with that.
The lesson isn’t “duplicate your business logic”. It’s that “never repeat yourself” is a guideline, not a commandment, and sometimes the version that’s worse on paper is the one that actually gives people what they asked for.
You can have a look at the Quotify templates if you want to see how they turned out.



