Case study · Headless Odoo
A headless storefront and B2B portal on Odoo 19
How we put a fast, ten-locale storefront and an authenticated B2B portal in front of Odoo 19 without moving the system of record — using a purpose-built API addon and a server-side proxy that keeps the ERP off the public internet.
- Client
- VARS Su Ürünleri (VARS Aquaculture)
- Sector
- Aquaculture import & distribution
- Our role
- Architecture, Odoo addon development, frontend, SEO, deployment
- Year
- 2026

The problem
VARS runs Odoo 19 Enterprise as its system of record: products, pricing, stock, CRM, sales orders and customs documents all live there, and none of it was going to move. The commercial requirement, though, was an export-facing storefront in ten languages, fast enough and structured enough to be found by international buyers, plus a portal where existing customers could see their own orders and documents.
Odoo's own website module could not carry that. Rendering a ten-locale catalogue through the ERP means the marketing site's speed and availability become the ERP's speed and availability, and it puts a business-critical database directly on the public internet. The alternative — copying the catalogue into a separate CMS — creates two sources of truth for price and stock, which is the failure mode the ERP exists to prevent.
The decision: headless, with the ERP behind a proxy
We kept Odoo as the single source of truth and put a separate frontend in front of it. The public catalogue is statically generated so it serves as fast HTML; authenticated portal pages are server-rendered per request because they show one customer's own data. Both run from the same TanStack Start application, which is why the split was possible without maintaining two codebases.
The important constraint is that the browser never talks to Odoo. Every call that crosses into the ERP goes through a server function acting as a backend-for-frontend. That single rule removes CORS configuration entirely, keeps Odoo's session cookie away from client-side script, and means the ERP does not need to be publicly reachable at all.
The security posture
Three decisions carry most of the risk, and each is recorded as an architecture decision record in the delivered repository:
- Lead intake is authenticated with a bearer token held only on the server. The browser posts to our own endpoint, which validates and filters the submission before forwarding it to Odoo. The token is never shipped to the client.
- Customer authentication proxies Odoo's own session: credentials go to a server function, Odoo authenticates them, and the returned session id is set as an HttpOnly, Secure, SameSite cookie. JavaScript on the page cannot read it.
- Checkout creates a draft sales order in Odoo and hands the buyer to Odoo's own payment portal. There is no payment SDK and no webhook receiver in the frontend, so it never touches card data and stays outside PCI scope.
The integration layer we built
The frontend talks to one thing: `varsco_content_api`, an Odoo 19 addon we wrote for this project. It exposes a versioned `/api/v1` surface rather than letting a frontend reach into Odoo's models directly, so the contract between the two systems is explicit and testable.
It covers the public catalogue and storefront products, tokenised lead writes into CRM, session-authenticated portal endpoints for orders and profile, delivery addresses, wishlist, product reviews, newsletter signup, order tracking and checkout — each with its own test module in the addon's suite.
Two further addons complete the picture: `midvex_sale_payment_link`, which produces the payment portal URL checkout redirects to, and `midvex_customs_op`, which surfaces customs and logistics documents inside the customer's portal — a requirement specific to import and export trading that no off-the-shelf storefront covers.
Ten locales, and the gates that keep them honest
The site ships in ten languages across 45 route files, including right-to-left Arabic and Persian. Translation coverage is not a matter of good intentions: the build runs five locale gates that fail on script leakage into the wrong language, stray emoji, incomplete translation coverage, incomplete article translation, and drift between the storefront and its source data.
Alongside those run unit tests, an end-to-end suite, a dedicated accessibility suite, and a contract test that pins the frontend's own types and schemas to the documented API contract, so a drift between what the frontend expects and what was agreed is caught in CI rather than in production.
Attribution that reaches the CRM
Every form submission captures the page, the locale and the campaign parameters that produced it, and the API addon maps the campaign, source, medium, country and language onto real CRM fields on the Odoo lead, recording the originating page on the lead itself. The practical result is that a deal in the pipeline can be traced back to the page and the language that generated it — the measurement most B2B sites claim and few actually wire up.
Stack and integration surface
- Odoo 19 Enterprise — system of record (products, pricing, stock, CRM, orders, customs)
- varsco_content_api — custom Odoo 19 addon exposing a versioned /api/v1 contract
- midvex_sale_payment_link · midvex_customs_op — custom Odoo addons
- TanStack Start (React 19 + Vite) — static storefront, server-rendered portal, BFF proxy
- TanStack Query · TanStack Router · Tailwind CSS · strict TypeScript
- Vitest · Playwright · accessibility suite · API contract test · five locale gates
- Docker, deployed to Dokploy
What was delivered
- A ten-locale public storefront and an authenticated B2B customer portal running against Odoo 19, with no second source of truth for price or stock.
- An Odoo instance that no longer has to be publicly reachable, and a frontend that stays out of PCI scope.
- A lead pipeline where every enquiry in the CRM carries the page, locale and campaign that produced it.
- An integration layer that is deliberately reusable: a further client site is a new frontend pointed at an Odoo instance running the same addon, not a rewrite.
Traffic and conversion figures are deliberately absent. Analytics were not live in the production build before September 2026, so any number quoted for the period before that would be an invention. Measured results will be added here once there is a full reporting period behind them.
Next phase
Marketplace order flow, so orders arriving from Turkish marketplaces land in the same Odoo pipeline as storefront and portal orders.
Running an ERP you cannot put on the public internet?
That is the usual reason a headless build makes sense. Tell us what your system of record is and what you need in front of it.