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.
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 loadmodels/— Python files defining models (database tables + business logic)views/— XML files defining UI: forms, lists, kanban boards, menus, actionssecurity/— CSV/XML files defining access rightsdata/— XML/CSV files with default data loaded on installstatic/— 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.Char→VARCHARcolumnfields.Integer→INTEGERcolumnfields.Many2one→ foreign key columnfields.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:
# 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.
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:
<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:
- OWL JS collects all field values from the form DOM.
- OWL makes a JSON-RPC POST to
/web/dataset/call_kwwith model name,writemethod, record ID, and the changed values. - Werkzeug receives the HTTP request and routes it to Odoo's JSON-RPC dispatcher.
- Odoo's dispatcher finds the model class in the registry and calls its
write()method. - The ORM validates the data, runs
@api.constrainsvalidators, and issues anUPDATESQL statement via psycopg2. - The Python method returns the result. Odoo serialises it as JSON and sends the HTTP response.
- 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.envis 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
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.
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.
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.
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.