Platform

Odoo 18 as a backend for Next.js frontends — the architecture we're betting on

Odoo's Python backend is battle-tested for ERP but its web client is not what modern SaaS users expect. We pair Odoo 18's ORM, ACL, and workflow engine with Next.js 16 frontends via a thin JSON-RPC layer. Here's the architecture and the tradeoffs.

24 May 202610 min readKrypton Forge Labs

The Krypton Forge Labs Platform — the multi-tenant SaaS engine that powers Paraslace and future vertical products — runs on Odoo 18 Community Edition as the backend. The frontend is Next.js 16. The two communicate through Odoo's JSON-RPC API with a custom middleware layer in between.

This is an unusual choice. Most Odoo deployments use the built-in web client, QWeb templates, and the Odoo module system for everything. We broke from that pattern deliberately. Here is why, how, and what we learned.

Why not use Odoo's web client

Odoo's web client is functional. It has shipped thousands of ERP deployments across the world. It is also not what a modern SaaS user expects in 2026.

The Odoo web experience is dense, form-heavy, and optimised for internal operators who learn the system over weeks. That is a reasonable tradeoff for an ERP. It is not a reasonable tradeoff for a product sold to textile manufacturers whose operators quit every eight months and whose owners check dashboards on a phone.

Next.js 16 gives us UX control, React Server Components with streaming and partial prerendering, a larger hiring pool (more React developers in India than Odoo developers), and proper subdomain-per-tenant SaaS routing.

The architecture

Browser (Next.js 16 App Router)
    │
    ├─ SSR/SSG: Server Components call KF Middleware
    ├─ Client: React Query → KF API Routes → KF Middleware
    │
    ▼
KF Middleware (Next.js API routes)
    │  Tenant resolution, JWT auth, rate limiting, ACL cache
    │
    ▼
Odoo 18 Community (JSON-RPC, port 8069)
    │  ORM, workflow engine, business logic, PostgreSQL
    │
    ▼
PostgreSQL 16 (one DB per tenant)

The middleware layer does things Odoo's JSON-RPC endpoint should not be asked to do: tenant routing (subdomain → Odoo database), JWT-based authentication, ACL caching (caches user access rights for 5 minutes, eliminating redundant round trips), and response shaping (stripping Odoo's technical fields).

// Next.js middleware — tenant detection and routing
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const TENANT_MAP: Record<string, string> = {
  "paraslace-demo.kryptonforge.in": "paraslace_demo",
  "textileco.kryptonforge.in": "textileco_prod",
};

export function middleware(request: NextRequest) {
  const hostname = request.headers.get("host") || "";
  const db = TENANT_MAP[hostname];
  if (!db) return NextResponse.redirect(new URL("/404", request.url));
  const response = NextResponse.next();
  response.cookies.set("kf_db", db, { httpOnly: true, secure: true });
  return response;
}

What Odoo does well as a backend

We evaluated four options: Django from scratch, Supabase + Postgres, Medusa.js, and Odoo Community. Odoo won on two things.

The ORM. Odoo handles multi-company data isolation (the company_id pattern), computed fields, domain filters, onchange methods, and constraint validation at the ORM level. This is the kind of business logic that takes months to rebuild correctly on a raw Django or Supabase stack.

# Odoo 18 — business logic that's free with the ORM
class MrpProduction(models.Model):
    _name = "mrp.production"
    _inherit = ["mail.thread", "mail.activity.mixin"]

    product_id = fields.Many2one("product.product", required=True)
    product_qty = fields.Float("Quantity", required=True, default=1.0)
    bom_id = fields.Many2one("mrp.bom", "Bill of Materials")

    @api.onchange("product_id")
    def _onchange_product_id(self):
        if self.product_id:
            self.bom_id = self.product_id.bom_ids[:1]
            self.product_uom_id = self.product_id.uom_id

The workflow engine. Odoo's base.automation and mail.activity provide a workflow engine that covers 80% of manufacturing business processes with zero custom code. Rebuilding this from scratch is months of work.

The pain points

JSON-RPC is an acquired taste. Odoo's calling convention — model name, method name, positional arguments — works but is verbose and poorly typed.

Field-level ACL is deep but opaque. Odoo controls access at model, record, and field level. The error message says "Access Denied" and nothing more. We built a debug view that shows which ACL rule blocked which field.

Odoo upgrades are a project. Moving from Odoo 17 to 18 required a full migration, custom module reinstallation, and three days of testing. Budget a week per major upgrade.

The module system is both a strength and a trap. Our rule: no community module without reading its source first. Modules are code. Review them.

When this architecture makes sense

Pairing Odoo 18 backend with Next.js frontend makes sense when you need ERP-grade business logic, your users expect a modern web experience, you have separate React and Python teams, and the Odoo module ecosystem covers 70-80% of your domain model.

It does not make sense for simple CRUD apps, teams small enough that two codebases are overhead, or products needing real-time features (Odoo's JSON-RPC is request-response).

What we'd do differently

Start with a thinner middleware. The initial version was too ambitious — ACL caching, response shaping, field-level filtering, rate limiting. The version we run now is thinner: tenant routing, JWT auth, and a passthrough to Odoo JSON-RPC.

Invest in the Odoo test suite early. Odoo's test framework is capable but underdocumented. The ORM migration from 17 to 18 would have been a two-day project instead of a week if we'd had a real test suite.

Odoo's backend is a decade of ERP engineering. Next.js's frontend is the current state of the web. The two together are more than either alone. But the integration layer is real work. Budget for it.

Tags

  • odoo
  • nextjs
  • saas
  • architecture
  • multi-tenant
  • erp