Platform
Building multi-tenant SaaS on Odoo 18, the Anti-Corruption Layer way
Why Krypto Forge Platform uses Odoo 18 as the ERP backbone but wraps everything in our own ACL. The trap of coupling product code to Odoo's data model, and how to stay free.
We made a specific architectural bet when building the Krypto Forge Platform: use Odoo 18 as the ERP backbone, but never let our product code touch Odoo directly. Everything goes through an Anti-Corruption Layer we own. It costs us upfront. It saves us repeatedly. Here's the why.
The two-platform problem
Every vertical SaaS we want to build (textile, leather, jewelry, food) has the same shape underneath. Multi-tenant accounting. GST compliance. Invoicing. Inventory at the SKU level. Payroll. Reporting. We can either build this from scratch for each vertical, or we can use an ERP that already has it.
Odoo 18, for all its quirks, has it. Accounting, GST, e-invoice, multi-company, RBAC, the works. Twenty years of mature ERP semantics. Building that ourselves would consume the studio for a year and produce something worse.
So the platform uses Odoo as the backbone. Two real benefits.
- Compliance and accounting come "free" in the sense that they're already there, tested, and maintained by a community.
- Multi-tenancy at the database level (one database per tenant) is well-trodden territory in the Odoo world.
The cost: Odoo's data model is sprawling, its conventions are its own, and direct coupling is a one-way door.
Why we don't let product code touch Odoo
The trap is obvious in retrospect. You start by writing a Next.js page that queries Odoo directly through XML-RPC or the JSON-RPC endpoint. It works. You write another. It works. Six months later, your product code is full of res.partner and account.move.line and sale.order references. Renaming them, replacing them, swapping the backend, all became impossible.
You no longer have a SaaS on top of Odoo. You have an Odoo skin.
We've seen this happen at several Odoo-on-frontend projects. The product code stops being about textile workflow and starts being about Odoo workflow. The vertical brand disappears under the ERP terminology. Every future change has to round-trip through Odoo's conventions.
The Anti-Corruption Layer is the antidote.
What an ACL actually is, in this context
The term comes from Eric Evans's Domain-Driven Design. The idea: when two systems with different models have to talk, you put a translation layer between them. The product side speaks its own language. The ERP side speaks Odoo's. The ACL translates.
For us, that looks like:
- A set of TypeScript service modules in our Next.js app.
- Each module exposes domain operations relevant to the vertical:
createOrder(),assignToKarigar(),generateInvoice(). - Inside the module, those operations translate into Odoo calls.
res.partner.create,account.move.create, the whole soup. - The product code never sees Odoo terms. It calls
createOrder. The ACL handles the mapping.
// In product code, never seen Odoo:
const order = await orderService.create({
customer: customerId,
items: lineItems,
deliveryDate,
});
// Inside orderService (the ACL):
async function create(input: CreateOrderInput): Promise<Order> {
const partner = await odoo.findOrCreatePartner(input.customer);
const odooOrder = await odoo.saleOrder.create({
partner_id: partner.id,
order_line: input.items.map(toOdooLine),
commitment_date: input.deliveryDate,
});
return mapFromOdoo(odooOrder);
}
The product code on the left is timeless. The Odoo-specific code on the right is contained.
What this buys us
Three properties, in order of importance.
Vertical-specific language. Paraslace doesn't make customers log into "Odoo Sales Order Management". They place a fabric order with their karigar's name on it. The product surface uses the textile vocabulary because the ACL hides everything that isn't textile.
The option to swap. If five years from now Odoo's licensing changes, or a better backbone appears, or we decide to rebuild parts of the accounting layer ourselves, the surface area to change is contained. We rewrite the ACL. The product code stays.
Testability. Mocking the ACL is easy. Mocking Odoo's XML-RPC quirks is not. Our test suites stub out the ACL, which means we can write hundreds of unit tests against the product logic without ever touching the ERP layer.
The cost is real. Every operation has to be defined twice: once in product terms, once in Odoo terms. That feels like duplication until the first time you save yourself a month by swapping how the ACL talks to Odoo.
Where Odoo lives, where it doesn't
A concrete map of which work goes where.
Odoo owns:
- Tenant database structure (per-tenant Postgres database).
- Accounting (chart of accounts, GST treatment, e-invoice IRP integration).
- Invoicing and payment journal entries.
- Inventory ledger at SKU level.
- User identity for back-office staff.
- Reports for the accountant.
Our Next.js layer owns:
- The customer-facing brand (Paraslace, future verticals).
- The vertical-specific workflow (textile order, karigar assignment, fabric width math, dye-lot batching).
- The user identity for customers and field staff (separate from Odoo's back-office users).
- The AI agents and automation surface.
- The mobile experience.
- All branding, all marketing, all distinct product surface.
The seam is the ACL. Product calls translate into Odoo calls. Odoo events propagate back through webhooks into our event stream.
The decisions we wrote down
We have two architecture decision records covering this:
- ADR-008 sets out the platform topology: Odoo backbone, Next.js front, DB-per-tenant.
- ADR-009 sets out the ACL pattern: no direct Odoo references in product code, all access through service modules, every domain operation has a single point of translation.
We write ADRs because the value isn't visible at the time. Six months from now, somebody new (or future me) will be tempted to "just" call Odoo directly because it's faster. The ADR is the reason to push back.
Every time we've broken our own ACL rule, we've paid for it within six weeks. Every single time. The discipline is the product.
The shape of multi-tenancy
A note on the tenancy model, since it interacts with the ACL.
We run database-per-tenant. Each tenant has its own Postgres database, both for our application tables and for Odoo. This is heavier operationally than a shared schema, and it pays back in three places:
- Isolation. A bug in tenant A's data can't touch tenant B's.
- Data residency. Some tenants need their data physically separated for compliance reasons. The per-DB model makes that a deployment choice, not a code change.
- Backups and restores. Per-tenant. Useful when one customer asks for a point-in-time restore without affecting others.
The ACL takes the tenant context as a first-class argument on every call. Every operation routes to the right database. If we get this wrong, two tenants' data could collide. Getting it right is non-negotiable.
The honest tradeoffs
Things that aren't pretty about this approach.
Latency. Going through the ACL adds a hop. We mitigate with caching of stable Odoo data (master data, product catalogues) and direct read-only queries on the tenant DB for reporting paths where the abstraction isn't worth it.
Odoo upgrades. Odoo releases major versions yearly. Each upgrade requires testing the ACL against the new conventions. Not a huge amount of work, but it's not free.
Onboarding cost. A new engineer joining the studio has to learn the ACL pattern before they can do anything useful. We pair on this.
We accept all three because the alternative (Odoo-coupled product code) is worse on a long horizon.
The takeaway
If you're building a vertical SaaS in 2026 and you've decided to use Odoo (or any large ERP) as a backbone, the single most important architectural decision is whether to expose it directly. Direct exposure is faster on day one and slower on every subsequent day. The ACL is slower on day one and frees you forever.
We chose the ACL. We've never regretted it. We've sometimes broken our own rule and always regretted that.
The right abstraction isn't the one that's most elegant. It's the one that survives the next change you don't see coming.
Tags
- odoo
- saas
- architecture
- acl
- multi-tenant