
Declare the ERP or CRM your application builds on
An internal tool is built on top of systems the company already runs. Wildo calls such a system a system of record: the place where some of the application’s own business records live. The application names each one once, as an instance: “our Odoo”, “the SAP finance system”. Every resource that reads from it, writes to it or copies from it refers to that name.
Where the system is and how to sign in to it belong to each environment, not to the code. Production points at the real ERP, a test environment at a sandbox, and a developer’s machine can state that it has no access at all. The application still starts there, and screens that need the ERP say it is unavailable instead of pretending it is empty.
When the application starts it checks that every declared system is described by a known vendor and fully addressed: a misspelled vendor or a missing password stops the start with a message naming it. It does not contact the ERP to do so, so an ERP outage never prevents the application from starting.
A system of record is not one of the application’s providers, such as its email or payment service. It is the company’s own deployment, reached with the application’s own service identity, and the records read from it belong to the organization that owns the application. Nobody’s personal account is used, and no person can connect their own account to it.
Example — Two Odoo databases
A group runs its French and German companies in two separate Odoo databases. The application declares two instances, odoo-fr and odoo-de, of the same vendor. They differ only in their addresses and keys, which each environment supplies under names derived from the two instance names.
For engineers
Wonder Todos declares its Odoo in wildo.saas.config.ts; the explanatory comments are omitted:
systemsOfRecord: {
odoo: {
vendor: 'odoo',
dialect: HttpApiTransportDialect.ODOO_JSONRPC,
partition: true,
personalDataOrigin: SystemOfRecordPersonalDataOrigin.NO_PUBLICLY_ACCESSIBLE_SOURCES,
},
},
The key, odoo, is the instance name: lower-kebab-case, at most 32 characters, because it becomes part of environment-variable names. A resource binds it with system: 'odoo', and startup refuses a binding naming an undeclared instance or a dialect that disagrees with the instance’s.
| Field | Decides |
|---|---|
vendor | which vendor facet describes the system (odoo, sap-s4hana, salesforce, netsuite, microsoft-dataverse, microsoft-dynamics-365-business-central, or odata for any other OData v4 service) |
dialect | how it is spoken to; one instance has one dialect and one base address |
credential.kind | how the application proves its identity; omitted when the facet offers only one kind, required when it offers several |
maxRequestsPerMinute | the pace shared by every resource bound to the instance, since the vendor counts them as one integration |
partition | whether this deployment’s records are confined per company; see confining a system of record by company |
facetAugmentations | packages whose additions to the vendor facet apply to this instance |
personalDataOrigin | whether personal data in it may come from public sources, which the privacy notice states |
Declaring the block requires an organization type that declares ownsApplication: rows read from a system of record are stamped with that organization’s id, so the ordinary per-row tenant check decides who sees them. An empty systemsOfRecord is refused; omit the block instead.
Supply it per environment
Each environment’s wildo.infra.<env>.config.ts either addresses the instance, as in the configuration schema’s own example, or declares it absent:
systemsOfRecord: {
odoo: { address: { baseUrl: 'https://acme.odoo.com', database: 'acme', login: 'bot@acme.com' } },
}
systemsOfRecord: {
odoo: { enabled: false },
},
The second form is Wonder Todos’ local configuration. Identity values (an address, a database, a login, a client id) live in this file, where they can be reviewed in a diff; the proof (an API key, a password, a private key) lives in the environment’s secrets.json. Which slots are needed is the vendor facet’s and credential kind’s to say. Nobody writes a variable name: each is derived as WILDO_SOR_<NAME>__<SLOT>, here WILDO_SOR_ODOO__BASE_URL, __DATABASE, __LOGIN and __API_KEY. The double underscore cannot occur in either half, so every name parses back to its instance and slot. Renaming an instance renames its variables, and wildo config sync reports the slots that are then missing.
An environment that omits a declared instance entirely is refused, both by wildo config sync and at startup: silence could equally mean “no access here” and “someone forgot the address”, and those need opposite behaviour. With enabled: false, the application starts, reads of bound resources answer 409 with SYSTEM_OF_RECORD_UNAVAILABLE_IN_ENVIRONMENT, and scheduled pipelines are not dispatched.
What startup checks, and what it does not
At startup the engine joins each declaration with its vendor facet and its environment, once, and every consumer reads the result. It refuses a vendor no facet describes (an alias such as microsoft-dynamics-365 is refused with the vendor id that carries the facet), a dialect the facet does not speak, a credential kind it does not offer, several offered kinds with none chosen, and a required slot the environment did not supply. It never dials the system: whether the remote answers is discovered by the first read, which reports a transport error.
The vendor facets are an opt-in library
The facets live in @wildo-ai/systems-of-record, not in the engine. An application that declares no system of record never receives them. Once systemsOfRecord is declared, wildo config sync (and every other command that regenerates provider artifacts) adds the library to each backend service and links it, then refuses to continue until it is installed, because a backend without it would refuse every instance.
Why a system of record is not a provider
The read lane and the connector lane share an HTTP transport and nothing else. pnpm check:systems-of-record-separation refuses any import from the system-of-record lane into provider or connected-account code, and the reverse. A system of record is never selected through the capability plane, never spends a person’s connected account, and its registry is not exposed to application code, which reaches an instance only through the resources bound to it.