2
1 Comment

Shipping a bilingual AI SaaS solo: the architecture behind a "bank-ready" business-plan engine

The interesting problem in "AI writes your business plan" was never the writing.

Any modern LLM produces a fluent, plausible business plan in one shot. That's the easy 80%. The problem is that a fluent business plan is worthless to the person who needs one. A bank's credit officer or an early-stage investor doesn't want prose — they want a document whose financials are internally consistent, whose market claims trace back to real sources, and that looks like it came out of a consulting firm instead of a chat window.

Getting from fluent to bankable is an engineering problem, not a prompting one. That gap is the entire product.

I'm the solo founder and only developer of Evaltrum, a bilingual (FR/EN) SaaS that generates investor- and bank-ready business plans and market studies. It's been live in production since May 4, 2026. Below is how it's actually built — the architecture decisions, the parts that were harder than they looked, and the lesson that has nothing to do with code.

The stack (deliberately boring)
Laravel 13 / PHP 8.4 on a single OVH VPS behind Nginx
MySQL for state, Redis for queues
Filament v4 for the admin panel (this alone saved me weeks of internal CRUD)
Stripe Cashier in live mode for subscriptions + one-shot purchases
Claude Sonnet as the generation engine
Tavily (web search) + Google Maps Places for grounding market data
Puppeteer driving headless Chrome for PDF rendering
PHPWord for the .docx export path
Resend for transactional email

No microservices. No Kubernetes. One box, a monolith, and a queue. As a solo dev, every piece of infrastructure I add is a piece I have to debug at 2 a.m. The boring stack is the competitive advantage — it lets one person ship features instead of babysitting orchestration.

The hard part: making an LLM produce a cited market study

The flagship module, Evaltrum Intelligence, generates a ~10-page market study. The requirement that shaped the whole design: every market claim has to be attributable to a source. "The French coworking market is growing" is a liability. "The market is growing [3]" with a working reference is an asset.

You cannot get that from a single prompt. LLMs hallucinate citations more confidently than they hallucinate facts. So the generation is an orchestrated pipeline, not a call:

A MarketStudyOrchestrator decomposes the study into sections, each with its own research intent.
Before any prose is written, a research phase hits Tavily and Google Maps Places and writes every retrieved fact into a SourceRegistry — a single structured store of {claim, url, retrieved_at}.
Generation is then constrained: the model writes around the registry, and can only reference source IDs that already exist in it. If it wants to assert something, the claim has to be backed by a registry entry or it doesn't survive.
The whole thing runs as queued jobs on Redis. A market study takes minutes, not milliseconds — trying to do it inside an HTTP request is how you get gateway timeouts and furious users. The user kicks off the job, gets an email when it's ready.

This inverts the usual naive flow ("generate, then try to find sources"). Sources come first; the narrative is fitted to them. It's slower and more expensive per report. It's also the reason the output survives contact with someone who actually checks.

The single-source-of-truth pattern that saved me

Every report ships in two formats: PDF and Word. The obvious, tempting design is two export paths — one that formats for Puppeteer, one that formats for PHPWord. That's also how you guarantee they silently drift apart, and how a customer ends up with a PDF and a .docx that say different things.

Instead there's one method — call it citedReport() — that returns the fully-resolved, source-linked report object. Both exporters consume that identical structure. The PDF renderer and the Word renderer are pure presentation layers over one canonical payload. They physically cannot diverge, because there's nothing to diverge from.

This turned out to be the highest-leverage architectural decision in the codebase, and it generalizes: when the same content has to appear in N formats, resolve the content once into a canonical form and make every format a dumb consumer of it. I now reach for this pattern reflexively.

The PDF pipeline nobody warns you about

"Just generate a PDF" is a trap. The two common routes — server-side PDF libraries and headless-browser rendering — trade off differently, and for anything that needs to look designed (covers, typography, charts), the browser wins. So Evaltrum shells out to Puppeteer driving headless Chrome, rendering the same HTML/CSS the browser would.

The gotchas that cost me real time:

Running headers/footers. Some documents need them, some (the market study) must not have them. I ended up passing a per-document HTML marker that the rendering layer reads to suppress header/footer injection. One flag in the markup, honored by the pipeline — cleaner than branching the renderer.
On-demand generation and cleanup. Word files are generated when requested and deleted right after they're sent. Report files are user data; leaving them on disk is both a storage leak and a privacy problem.
Fonts. Headless Chrome renders with whatever fonts are on the box. If they aren't installed, your beautiful Playfair Display cover silently falls back to something that screams "template." Pin your fonts on the server.
Bilingual without an i18n framework

Evaltrum is fully FR/EN, and I deliberately did not build a translation-key layer for the marketing surface. For pages where the surrounding chrome is already bilingual, a single parameterized view with conditional blocks (@if($en) … @else@endif) is far less overhead than maintaining two parallel key files that rot out of sync.

For the app itself, language is resolved from the right source depending on context: the project's language for generated documents, the user's locale for billing screens and email. Getting this boundary wrong means a French user gets an English invoice — small bug, large trust hit. Being explicit about which language authority governs which surface removed a whole class of subtle bugs.

Opinionated take: full i18n frameworks are correct at scale and premature overhead for a solo founder shipping two languages. Match the tool to the actual cardinality of the problem.

The lesson that isn't technical

Here's the part I'd want to read on Indie Hackers.

Three months post-launch, with the product genuinely working — real generation, live payments, a polished admin, clean exports — the bottleneck was not the product. It was distribution, and specifically domain authority.

A young domain ranks on page 2–3 behind incumbents no matter how good the pages are. My instinct as a builder was to build more — more pages, more content, another module. That instinct was wrong every single time. On a low-authority domain, piling on pages dilutes authority; it doesn't earn it. The lever is referring domains and social proof, not code.

The uncomfortable truth for technical founders: once the product works, more engineering is usually procrastination. The next unit of progress is almost always distribution — and distribution is the thing we're worst at and most eager to avoid by opening the editor again.

I still catch myself proposing a new build when the honest answer is "go get one editorial backlink and talk to five potential resellers." Writing this article is, itself, me finally taking my own advice.

If you're a builder, the architecture bits above are yours to steal — the source-registry-before-generation flow and the single-canonical-payload export pattern in particular have paid for themselves many times over.

And if you're on the other side of it — someone with an idea who needs the document, not the lecture on how it's made — that's literally what I built Evaltrum to do. Idea in, structured, sourced, bank-ready plan out, in about half an hour.

Happy to answer anything technical in the comments — the orchestration layer especially.

on August 2, 2026
  1. 1

    The shift from building to distribution makes sense. The more consequential conclusion seems to be narrowing the bottleneck specifically to domain authority.

    What evidence made you confident that authority is the constraint, rather than something downstream like search intent, trust, or conversion once people actually reach Evaltrum?