Most side projects put their business logic in the application layer. We did the opposite, and it was the single best architectural decision we made.
The problem shape
CarInsight evaluates used-car listings in the German market and produces a decision report: is this price defensible, what typically breaks on this model, and what will it cost to own. The output is not a data dump. It is a judgement, and a judgement has to be defensible.
That constraint drove everything.
Stack
Next.js 14 App Router, TypeScript, Tailwind, shadcn/ui on the front. Postgres on Supabase. Stripe Checkout for payment. Deployed on Vercel.
Nothing exotic. The interesting part is what is not in there.
The report engine does not live in the app
The scoring, the comparison logic, the risk weighting, the cost model — none of it runs in a Next.js route handler. It runs as a separate orchestration layer with an orchestrator and a set of subworkflows.
Three reasons:
Auditability. When a report says a price is 12 percent above the comparable set, we need to reconstruct months later which listings formed that set and when they were pulled. A serverless function that computed it inline and returned JSON gives you nothing to replay.
Independent iteration. Scoring logic changes far more often than the checkout flow. Coupling them means every weighting tweak is a full app deploy.
Failure isolation. A report generation failure should not take down the page where someone is trying to pay you.
The reliability pattern
Anything user-facing goes through four stages:
- Evidence pack. Gather sources first, with timestamps. Nothing downstream may introduce a fact that is not in the pack.
- Generator. Produce the report from the pack only.
- Critic. A separate pass checks each claim against the pack and flags anything unsupported.
- Schema gate. Structural validation. Malformed output never reaches a user.
The rule underneath all of it: no source, no claim. If a figure cannot be sourced, the section stays incomplete rather than getting filled with something plausible. This is harder than it sounds, because plausible-and-wrong is exactly what generative systems are good at producing.
What actually turned out hard
Not the infrastructure. Euro-denominated claims.
"This model costs roughly X per year to maintain" is trivial to generate and nearly impossible to source properly. We ended up splitting cost components into two buckets: derivable and estimated.
Derivable means there is a formula or an official reference. German vehicle tax, for example, is defined in statute — engine displacement and CO2 emissions, fixed rates. That is not a prediction, it is arithmetic. We implemented it as code and validated it against the official calculator until deviation hit zero.
Estimated means everything else, and it gets labelled as an estimate with the method disclosed.
The uncomfortable finding from this exercise: the categories where a buyer most wants a hard number are exactly the categories where hard numbers do not exist. Public statistics on model reliability, for instance, generally do not distinguish between engine variants of the same model. So a claim at engine-variant granularity cannot cite them, however much you would like it to.
We chose to say so in the product rather than paper over it.
Takeaway
If your product is a judgement rather than a lookup, treat provenance as a first-class architectural concern rather than a logging afterthought. It constrains your stack in useful ways.
More on the product at CarInsight.
Top comments (0)