
Keep each subsidiary to its own company's records
A group often runs several companies inside one ERP: one Odoo database holding the French, German and Spanish entities, for example. Without confinement, everybody in the application would see every company’s customers and invoices, because all of them belong to the same organization in the application.
An operator maps each company in the ERP to a unit of the organization. A person granted a role on a unit then works within that company: their lists show its records and the records that belong to no company, a record of another company answers as if it did not exist, a record they create lands in their company, and an action such as posting an invoice is refused on another company’s invoice. The restriction is sent to the ERP as part of every query, so totals and pages are correct and nothing is fetched only to be thrown away.
A company that nobody has mapped is read by nobody, not even by organization-wide administrators. Every mapping, move and removal is made by an application super-administrator, carries a written justification, and is recorded with the unit before and after.
Example — Finance managers per subsidiary
A group’s Odoo holds two companies. The operator maps company 1 to the unit “Acme SA” and company 2 to “Globex SARL”. A finance manager granted on the Acme unit lists invoices and sees Acme’s and the shared ones; opening a Globex invoice answers “not found”, and posting it is refused: the only call that reaches Odoo is the read that found the invoice belongs to Globex. A third company, created in Odoo later and not yet mapped, appears to nobody until the operator maps it.
For engineers
Partitioning is a property of the deployment, declared on the instance; how the vendor partitions is the facet’s. Wonder Todos’ declaration, comments omitted:
systemsOfRecord: {
odoo: {
vendor: 'odoo',
dialect: HttpApiTransportDialect.ODOO_JSONRPC,
partition: true,
personalDataOrigin: SystemOfRecordPersonalDataOrigin.NO_PUBLICLY_ACCESSIBLE_SOURCES,
},
},
The Odoo facet declares the partition as the company_id field, read as Odoo’s id-name pair and compared as an integer. Both were measured against a live multi-company Odoo: a domain comparing the id as text matches nothing and reports no error, so a wrong codec would have shown members empty lists that looked correct. A record whose company_id is unset is shared and belongs to every caller. partition: true on a vendor whose facet declares no partition is refused at startup, naming the vendor.
Map companies to units
Assignments are an application-scoped engine resource, systemOfRecordPartitionAssignments, reached at /applications/{applicationId}/system-of-record-partition-assignments by an application super-administrator. Each row names the instance, the remote key in canonical text and the unit, with a justification. One remote partition maps to at most one unit. A move is its own operation, which states the unit it expects to replace and is refused if another operator moved it first. Every transition is written to the audit trail with the unit it left.
Gate reads so confinement actually applies
Unit narrowing is additive: a role a person holds across the whole organization wins over one held on a unit. A read-through resource bound to a partitioned instance must therefore gate its reads on a role above the default member role, or startup refuses it, because every member would otherwise reach every company. Wonder Todos’ billableServices and customerInvoices gate reads, writes and the invoice action on ORG_MANAGER; a person granted that role on one unit only is confined to that unit’s company.
What each operation does under confinement
| Operation | Confinement |
|---|---|
| List, count, search | the predicate is pushed to the ERP: for Odoo a domain matching the assigned company ids or an unset company_id, for OData an eq or-chain, for SuiteQL or SOQL a statement clause |
| Read by key | a record of another company answers as not found |
| Create | lands in the partition of the caller’s unit; the partition field is never accepted from the caller |
| Update, delete, record action | the record is read first; another company’s record is refused as absent, and only that read leaves the application |
| Change to a shared record | allowed only to an organization-wide caller, since it affects every company |
| Copy-in pipeline | each copied row is stamped with the unit its company is assigned to (partitionUnitLocalField) and keeps the remote company key (partitionKeyLocalField); both are required on a partitioned instance. A row whose company is unassigned after it was copied is read by nobody until the company is assigned again |
| Outbound pipeline | a created row is placed by its unit; an update is sent only if the record’s current company is the row’s unit’s |