Ad – 728×90
🐍 Introduction

Odoo Architecture – How the Stack Works

Understanding Odoo's architecture is essential before writing your first module. Odoo uses a 3-tier architecture: a PostgreSQL database, a Python application server (built on Werkzeug), and a browser client (built on OWL JS). Everything communicates through a JSON-RPC API. This page walks through each layer, how a request flows through the system, and what role your custom code plays in each part.

⏱️ 20 min read 🎯 Beginner 📅 Updated 2026

Three-Tier Architecture

Odoo's stack is divided into three distinct tiers, each with a clear responsibility:

  • Tier 1 — PostgreSQL: the data store. Every Odoo model maps to a database table. All persistent data lives here.
  • Tier 2 — Python Server: the application server. Built on Werkzeug (a WSGI toolkit). Processes HTTP requests, runs the ORM, executes business logic, and returns responses.
  • Tier 3 — Browser Client: the UI. Built on OWL JS. Renders views, handles user interaction, and communicates with the server via JSON-RPC.
Text
Browser (OWL JS + XML Views)
      ↕  JSON-RPC / HTTP
Odoo Server (Python + Werkzeug)
      ↕  SQL (psycopg2)
PostgreSQL Database

Your custom Python code lives in the middle tier. Your XML view files define what the browser tier renders. Your data is stored and queried from the database tier via the ORM — you rarely interact with PostgreSQL directly.

The Module System

Every feature in Odoo is a module (also called an addon). Core Odoo features — base, web, mail, account, sale, stock — are all modules. Your custom code is also a module.

A module is a directory with a specific structure:

  • __manifest__.py — declares the module name, version, dependencies, and files to load
  • models/ — Python files defining models (database tables + business logic)
  • views/ — XML files defining UI: forms, lists, kanban boards, menus, actions
  • security/ — CSV/XML files defining access rights
  • data/ — XML/CSV files with default data loaded on install
  • static/ — JavaScript, CSS, images for frontend components

Modules declare dependencies in their manifest. Odoo loads them in dependency order at startup. If your module depends on sale, Odoo ensures sale is loaded first.

The ORM Layer

Odoo's ORM (Object-Relational Mapper) is the core of the Python tier. Python classes extending models.Model map directly to PostgreSQL tables:

  • fields.CharVARCHAR column
  • fields.IntegerINTEGER column
  • fields.Many2one → foreign key column
  • fields.One2many → virtual (no column; traverses the FK from the other side)
  • fields.Many2many → junction table

You never write raw SQL for standard CRUD operations. The ORM handles it:

Python
# Creating a record
partner = self.env['res.partner'].create({
    'name': 'Acme Corp',
    'email': 'info@acme.com',
})

# Searching
orders = self.env['sale.order'].search([
    ('state', '=', 'sale'),
    ('partner_id.country_id.code', '=', 'US'),
])

# Domain syntax: [('field', 'operator', 'value')]
# Operators: =, !=, >, <, >=, <=, like, ilike, in, not in, child_of

self.env is the environment — it gives you access to the database cursor, the current user, the request context, and the model registry. It's the central object you interact with in all Odoo Python code.

Ad – 336×280

The View Engine (XML Views)

Odoo's UI is not built with React, Vue, or Django templates. Instead, views are defined as XML records stored in the database (in the ir.ui.view model). When a user opens a form, the browser asks the server for the view definition, then OWL JS renders it.

View types: form, list (tree), kanban, search, graph, pivot, calendar, gantt.

You define views in XML files in your module's views/ directory. Odoo loads them into the database on module install/update:

XML
<record id="view_library_book_form" model="ir.ui.view">
    <field name="name">library.book.form</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
        <form>
            <sheet>
                <group>
                    <field name="name"/>
                    <field name="author_id"/>
                    <field name="price"/>
                </group>
            </sheet>
        </form>
    </field>
</record>

This XML record, when loaded, creates a row in the ir.ui.view table. The browser fetches this definition via JSON-RPC and OWL renders it as a form in the UI.

The RPC Layer

The browser client communicates with the Python server via JSON-RPC POST requests. The primary endpoint is /web/dataset/call_kw, which accepts a model name, method name, and arguments.

Additional endpoints: /web/action/load (load an action), /web/menu/load_menus (load navigation menus), and in Odoo 19+, a REST API at /odoo/api/v1/ for external integrations.

In practice, you don't call these endpoints manually. OWL JS's built-in rpc service handles all communication. In Python, you write methods on your model — OWL calls them automatically when the user interacts with the UI.

Request Lifecycle

Here is what happens when a user clicks Save on a form:

  1. OWL JS collects all field values from the form DOM.
  2. OWL makes a JSON-RPC POST to /web/dataset/call_kw with model name, write method, record ID, and the changed values.
  3. Werkzeug receives the HTTP request and routes it to Odoo's JSON-RPC dispatcher.
  4. Odoo's dispatcher finds the model class in the registry and calls its write() method.
  5. The ORM validates the data, runs @api.constrains validators, and issues an UPDATE SQL statement via psycopg2.
  6. The Python method returns the result. Odoo serialises it as JSON and sends the HTTP response.
  7. OWL receives the response and re-renders the form to reflect the saved state.

This lifecycle makes clear where your code hooks in: you override or extend Python model methods (write, create, unlink, custom methods) to add business logic. The browser and database tiers handle themselves.

📋 Summary

  • Odoo uses a 3-tier architecture: PostgreSQL → Python/Werkzeug server → OWL JS browser client.
  • Every feature is a module (addon) with a defined directory structure: manifest, models, views, security, data.
  • The ORM maps Python classes to PostgreSQL tables — no raw SQL for CRUD. self.env is the central access point.
  • Views are XML records stored in ir.ui.view, loaded from module XML files, rendered by OWL JS.
  • Browser and server communicate via JSON-RPC. Your Python model methods are the main extension point.

FAQ

Does Odoo use Django or Flask? +

Neither. Odoo has its own WSGI server built on Werkzeug — a lower-level WSGI toolkit. Odoo's HTTP routing, session handling, and request processing are all custom-built within the Odoo codebase. It pre-dates and is separate from Django and Flask. If you know Django, you'll recognise some patterns (ORM, models, views) but the implementation is entirely different.

What database does Odoo use? +

PostgreSQL only. MySQL and SQLite are not supported. Odoo relies on several PostgreSQL-specific features: advisory locks, materialised views, specific SQL functions, and the psycopg2 driver. This is a deliberate architectural decision — PostgreSQL is the only supported database engine for Odoo.

Can I use Odoo's ORM without the rest of Odoo? +

No. Odoo's ORM is tightly integrated with the module system, the environment (self.env), the model registry, the access control layer, and the server startup process. It is not a standalone library and cannot be extracted for use outside of an Odoo runtime.

What is ir.model and ir.ui.view? +

They are Odoo meta-models — models that store information about other models and views. ir.model stores a record for every installed model (its name, description, fields). ir.ui.view stores every view definition as a database record. This is a core Odoo design principle: everything is a record. Menus, actions, access rights, email templates, scheduled actions — all are stored as database records, not static files.