Security posture

What Metrilens cannot do.

It cannot write to your Odoo — there is no code path that could. It cannot read a model outside a fixed list of 45. The API key you hand over never leaves your server. Each of those is a claim about a named file in a codebase you host yourself, so each one is checkable. The limits of the promise are on this page too, further down, because they are the part that decides whether you should believe the rest.

Enforced in code

There is no write path to write with

The one endpoint that talks to Odoo accepts exactly eight actions: search_read, search_count, search, read, read_group, get_version, get_modules, fields_get. Anything else is rejected before Odoo is contacted. execute_kw — Odoo's generic "call any method" entry point — is not one of the eight.

The request also never carries an Odoo method name. The server-side client implements those reads and nothing else, so there is no name a caller could smuggle in. No create, write or unlink call exists anywhere in the codebase.

src/app/api/odoo/route.tssrc/lib/odoo/odoo-client.ts

A closed list of 45 models

Reads are allowed against 45 named Odoo models — sales orders, invoices, stock quants, BOMs and so on. A request naming anything else is answered with HTTP 403 before any connection to Odoo is made.

That list is a single file, and it is the same list the read-only Odoo connector behind the AI analyst uses. There is no second, looser list.

src/lib/odoo/allowed-models.ts

The API key never reaches a browser

The key is read from config/odoo.json by server code only, and used to build the request server-side. The status endpoint the dashboard calls returns the Odoo URL, the database name, the user name and the numeric user id — never the key.

src/lib/odoo/config.tssrc/app/api/auth/route.ts

Encrypted at rest — once you set a key

With the SECRET_KEY environment variable set, every file under config/ is written as AES-256-GCM with a fresh random IV and an authentication tag. That covers the Odoo credentials, the mail settings and the AI settings.

Without SECRET_KEY those files are written as readable JSON. The encryption is real; it is not automatic.

src/lib/auth/crypto-config.ts

Caps on volume, not just on verbs

A request that omits a row limit, or asks for an implausible one, is clamped to 100,000 rows — so a single call cannot be used to dump a table. Callers that genuinely need everything page through with an offset.

Each client IP gets a bucket of 240 requests that refills at four per second. It is sized to survive a person browsing tabs and to stop a script before it can hurt your Odoo.

src/app/api/odoo/route.tssrc/lib/auth/rate-limit.ts

Browser-side hardening

Every response carries a Content-Security-Policy limited to its own origin, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, a strict-origin-when-cross-origin referrer policy, and a permissions policy denying camera, microphone and geolocation. API responses additionally carry HSTS.

The policy does allow inline and evaluated scripts from our own origin, because the framework needs them. It is a useful CSP, not a strict one.

next.config.mjssrc/proxy.ts

Where the promise ends

A read-only proxy solves one problem well and leaves others untouched. These are the ones an IT person should weigh before issuing a key.

The API key itself is full access to your Odoo

An Odoo API key carries the complete rights of the user it belongs to — reading and writing. Read-only is a property of this tool, not of the key you hand it. If the key leaks, whoever holds it is not restricted to what Metrilens does with it. The fix is on your side and it is worth doing: create a dedicated Odoo user whose groups are read-only, and issue the key for that user. Then the key is as harmless as the tool.

Encryption at rest is only as strong as SECRET_KEY

No SECRET_KEY, no encryption — the credentials sit on disk as JSON. And the encryption key is derived from that environment variable by hashing it, so a short secret makes a weak key. Anyone with the server's filesystem and environment has your Odoo credentials either way; encryption at rest protects backups and snapshots, not a compromised host.

Read-only does not mean confidential

The dashboard shows revenue, margins, customers, and — with the HR module — headcount and contract data. Whoever can reach it can read all of it. Access is a session cookie, required on every API route except the few that must work before a login exists (login itself, the health check, invite acceptance, the setup probe). One environment variable, AUTH_BYPASS=true, switches that check off completely. It exists for a trusted local network and belongs nowhere else.

An install with no users has a legacy back door

On a fresh single-admin install, before any dashboard user has been created, the status endpoint still hands out a session — that is the old pre-login behaviour, kept so existing installs keep working. Creating at least one dashboard user closes it. Do that before the instance is reachable from anywhere but your own machine.

Share links are bearer tokens

A share link reads through the same read-only proxy without a login. Anyone who has the URL sees what the link exposes, until the token expires or you delete it. Treat a share URL like the figures behind it.

Rate limiting is protection, not access control

The limit lives in the process memory of one instance: a restart resets it, and several colleagues behind one office IP share a bucket. It is there to keep a runaway script from hammering your Odoo. It is not a security boundary, and the forwarded-IP header is only trusted when you explicitly set TRUST_PROXY=true behind your own reverse proxy.

The AI features are opt-in, and they are an outbound door

Configure nothing and there is no AI and no outbound traffic. Configure a hosted provider and the figures placed in a prompt are sent to that provider. If that is unacceptable, point the same setting at a local model runner — Ollama or LM Studio — and the data stays on your hardware.

With MCP on, the model chooses what leaves

The AI chat can be given live access to Odoo through the read-only MCP server. It stays read-only and stays inside the same 45-model list, but the selection is no longer the figures on your screen — the model decides which records to fetch. Paired with a hosted provider, those records go to that provider. The setting states this at the switch; it is repeated here because the two settings are usually changed weeks apart.

One install, one Odoo

Metrilens is single-tenant by design: an instance reads one connection from its own config directory. That is a limitation if you wanted one deployment for several companies, and it is the reason there is no shared database holding other people's credentials.

This is a description of code, not a certificate

Nothing here has been signed off by an external auditor. What it has instead is that you host it: the files named on this page are in the source you run, and you can read them before you trust them.

Where it connects out to

Metrilens runs on your own machine. Usage statistics, when switched on, are written to a file in your config directory and are not sent anywhere. The complete list of outbound destinations:

DestinationWhen
Your Odoo instanceAlways — that is the product
An AI providerOnly if you configure a hosted one
Your SMTP serverOnly if you enable the email digest
A license serverOnly if a license server address is configured

The other question buyers ask first: Odoo versions.