metadata jsonb as the unstructured escape hatch, but you usually want more than a bag of untyped values. Custom fields turn a declared subset of that space into fields that are validated on write, visibility-aware, and PII-aware, without forking the platform.
Custom fields live in @voyant-travel/custom-fields. Definitions are data, not code: they are rows in custom_field_definitions, created and managed at runtime through the module’s API rather than declared in a source file and shipped in a build.
Ownership
Every definition has an owner, and the owner is what keeps one party’s fields from colliding with another’s:operatorCustomFieldDefinitionOwner and createAppCustomFieldDefinitionOwner(...) construct owners, and assertCustomFieldDefinitionOwner(...) enforces that a writer only touches definitions it owns. An app cannot edit operator fields, and vice versa.
Field types
customFieldTypeEnum is the closed set:
varchar, text, double, monetary, date, boolean, enum, set, json, address, phone.
Each definition also carries a lifecycle state (customFieldLifecycleStateEnum), so a field can be retired without deleting historical values.
How the registry resolves
Definitions come from thecustom_field_definitions table, scoped by owner. There is no code-declared tier to merge with and no build step: creating a field is an API call, and it takes effect without a deploy.
Because definitions live in the database, the registry is resolved per request from a db handle rather than being a boot-time constant. The platform passes it into the entity services that validate on write.
Managing fields
Definitions are backed by thecustom_field_definitions table (TypeID prefix cfdf) and managed through the admin API under /v1/admin/relationships/custom-fields. An app manages its own namespaced definitions the same way, through its granted scopes.
A definition payload looks like this. The runtime surface uses
fieldType, isRequired, and isSearchable, the runtime equivalents of the code-declared type, required, and visibility.search.
entityType and fieldType are immutable after creation: changing the entity type would orphan every stored value, and changing the field type would reinterpret stored JSON under the wrong type, so both are omitted from the update surface. Renaming key is allowed; the service migrates the stored JSON keys in lockstep.
Runtime definitions cover the CRM entity types (
organization, person, quote, activity), and their runtime field types (varchar, text, double, monetary, date, boolean, enum, set, json, address, phone) map onto the canonical code-declared types (varchar to text, double to number, enum to select, set to multiselect, address and phone to json). Code-declared fields are broader: their entity is any string. On a (entity, key) collision the code-declared field wins, so a deployment’s code contract is never overridden by a runtime edit.Validation on write
When an entity is written, the service validates the incoming custom-fields payload against the registry withvalidateCustomFields(registry, entity, input). It rejects unknown keys, errors on missing required fields, and type, options, and custom-rule checks every present value. It returns the cleaned value and any errors; the caller persists value into the entity’s custom-fields JSON only when ok is true.
Channel visibility
Each field declares whether it surfaces in exports, invoices, and search, so those readers consult the registry instead of dumping or hiding everything. Defaults are conservative: visible in exports, hidden from invoices and search.A
monetary field stores money the same way the rest of the platform does: integer minor units plus an ISO-4217 currency ({ amountCents, currency }). See Data models for the money convention.Where custom fields are available
Custom-field validation is wired into the write paths of the entities that opt in, today including people and organizations, bookings, and proposals. An entity adopts custom fields by carrying a custom-fields JSON column and consulting the injected registry on write, so the set can grow without changing this contract.Next steps
Data models
The entity columns, money convention, and JSON storage behind custom fields.
Build an app
The same drop-a-folder discovery seam, applied to whole capabilities.
Relationships
People and organizations, the most common home for custom fields.
Configuration
How a deployment declares and injects its own config and capabilities.