Skip to content

Multi-company

One word on a model and everything changes: a column appears, every read is filtered, every create is stamped, and some writes become impossible.

registry.register_model(
"books_invoice",
module="books",
fields=[...],
multi_company=True,
)

What it does, exactly

On a real database, with two companies and one invoice in each:

columns of books_invoice: ['id', 'number', 'company_id', 'created_at', ...]
created with no company_id -> company_id filled: 1
company 2 sees -> ['TG-001']
company 1 sees -> ['FR-001']
consolidated view (both companies active) -> ['FR-001', 'TG-001']
writing into a company the user may not reach -> CrossCompanyWriteRefusedError

Four behaviours, none of them written by you:

  1. A company_id column is added to the model. You do not declare it.
  2. It fills itself at create time, with the user’s current company.
  3. Every read is filtered on the active companies, in the ORM, so everywhere: the API, a background job, an export. This is not a screen filter.
  4. Writing into a company the user cannot reach is refused, including by an indirect path. That is the project’s interdiction #35, held by the kernel.

The consolidated view (several companies active at once) shows both, which is what a “all companies” selector is expected to do.

When to set it

Where there is a legal or accounting boundary, and nowhere else.

That is the project’s decision D1, and it is deliberately restrictive. An invoice, a journal entry, an order, a payslip belong to a legal entity: two companies do not share their books, and an invoice belongs to the one that issued it.

A catalogue product, a partner, a tag, a mail template: no. Those are shared reference data, and duplicating them per company is data entry with nothing in return.

What it means for your screens

Nothing to do. The company selector in the header, the filtering, the stamping at create: the generic engine handles them because the declaration is on the model. That is why multi-company security lives in the ORM and not in the endpoints: a module that forgot a check cannot leak, because it has nothing to check.

And if you get it wrong?

Moving a model from single to multi is a migration: the column is added, and somebody has to decide which company the existing rows belong to. That is a data migration, not a flag you flip one afternoon. The other way round, removing the flag, leaves an orphan column and data that becomes visible everywhere at once.

Which is why the question deserves to be asked before the model’s first line, and why the default answer is no.