← Work

Freedom Marine · CPQ

952 rules nobody had ever written down.

A multi-brand boat configurator and quoting platform. The sales team builds a boat, prices it in CAD and sends the quote, with every step tracked in the CRM. It replaced spreadsheet quoting by encoding each manufacturer's options, dependency rules and pricing logic as data.

Role
Product lead & builder
Stack
Next.js 16 · React 19 · TypeScript strict · Supabase Postgres · Vercel
The problem

The rules were real. They were just never written as rules.

A manufacturer's option list looks like a catalogue. It is actually a rulebook in disguise: a footnote saying a cushion set only fits two of the five sofa versions, a colour code meaning two deck layouts can never coexist, a "see note 4" that turns out to be mandatory hardware. Salespeople carried those rules in their heads and in spreadsheets, and quotes went out wrong.

So the work was not building a form. It was turning implicit knowledge into data a system can enforce on every click — and doing it for brands whose commercial logic has nothing in common.

What is in the system

1,016Equipment items, 3,933 price rows
952Dependency rules, Axopar alone
10,660Golden pricing test cases
220Checksummed database migrations
Architecture

One application, one database, nothing hard-coded.

All catalogue, rules, prices and step order live in Postgres rather than in code, so a new model year is a data change and a new brand is configuration — not a release.

LayerWhat it does
Front endConfigurator, sales dashboard and admin panel. 14 top-level steps, order and visibility configurable per brand.
ServerQuotes, server-rendered PDF, CRM sync, scheduled price changes, lead lifecycle, three cron jobs.
DatabaseCatalogue, prices, rules, step flow, quotes and audit log, with row-level security and roles.
PricingOne pure module holding every formula, with a pluggable scheme per model.
IntegrationsCRM and transactional email, isolated behind their own layer with idempotent writes.
Decisions under constraint

Six times the obvious answer was the wrong one.

The interesting part of this build is not the feature list. It is where the first approach failed and what replaced it.

  1. 01

    Four AI parsing approaches could not read the manufacturer lists

    Constraint · Rules encoded in footnotes, colour coding and cross-references

    I tried to parse the lists automatically four different ways. None was reliable enough to trust with pricing. The lists are implicit by design — they assume a human who knows the product. So I stopped trying to automate the reading and moved the effort to where it held: a typed rules model in the database, generators that turn a reviewed list into rules, and per-model overrides on top of global ones. Interpretation stayed human; everything after it became data.

  2. 02

    Two brands whose pricing has nothing in common

    Constraint · One engine, two unrelated commercial logics

    One brand prices from a European list through configurable formula steps — margin, markup, duties, freight, local standards. The other builds cost up from hull retail with carry and buy-forward, version deltas, fixed costs and engine MSRP. Rather than branch the code, pricing became a scheme attached to each model, with all formulas in one pure module. The database rejects one brand's pricing fields on the other's model, so the two cannot be mixed by mistake.

  3. 03

    Options that appeared and vanished on their own

    Constraint · Cascading dependencies the salesperson could not predict

    Enforcing rules correctly is not enough if the screen changes under someone's hands. Every click goes through a planner that computes the whole consequence first and shows it: what comes in, what goes out, why, and the new total. Where a rule allows several options, the salesperson chooses, and confirm stays disabled until they do. Cascades are computed against the final state, so swapping one version for another keeps what both require. Undo restores everything from before the click.

  4. 04

    No exchange rate meant no price, not a guess

    Constraint · One source of truth for FX

    The active rate is set by an admin and lives in one table. If no rate is active, the configurator refuses to price rather than fall back to a stale number. A quote that is quietly wrong is worse than a quote that does not appear, and the sales team needed to be able to trust the figure without checking it.

  5. 05

    The CRM model changed three times in three months

    Constraint · A moving integration target

    The shape went from one lead per quote, to deals, to leads with the configurator as source of truth, and finally to one lead per contact. Because the integration had been built as an isolated layer from the start, each change was contained there and never reached the configurator. Sends carry an idempotency key, so a retry never duplicates a lead or an email, and external writes are blocked outside production.

  6. 06

    Prices change while quotes are open

    Constraint · A live system with no separate staging database

    An admin schedules a price change with an effective date. Database triggers invalidate the affected open quotes, the PDF is regenerated with an "old pricing" watermark, and send and edit are disabled on them. The wider constraint — changing a live system without a staging copy — is handled with feature flags per brand, checksummed migrations, and a backup plus a reversal script for every change to live data.

How it was built

AI wrote most of the code. The judgement was the job.

About 70,000 lines of TypeScript, roughly 630 hours of development effort across eleven calendar months — about sixteen working weeks of one full-time developer. That ratio is the point: the code is generated and reviewed with AI under human direction, which is what made this scope reachable on this budget.

What does not delegate is knowing where the model will be confidently wrong. Pricing is the clearest case: formulas live in one pure module, and 10,660 golden cases compare every output against stored snapshots, including a set built to reproduce the client's own pricing calculator line for line. A sweep test replays every card and click combination for one model, so what a card promises is exactly what the click does.

Speed is held in place by release controls rather than process. Every change is a pull request with its own preview deploy. Data changes go through numbered migrations registered with a checksum. New brands ship behind feature flags.

What I'd do differently

The part I'm not happy with

The main configurator component grew to roughly 8,500 lines. It works and it is tested, but it slows down anyone new reading the code, and that is a cost I created by moving fast on a component that kept absorbing steps. It is being split step by step as each one is touched, rather than in one risky rewrite.

Have a product with rules like these?

Configuration, pricing and quoting where the logic is real and nobody has written it down yet.