Skip to main content
The common client ask is “add a few fields to bookings, people, or products.” Every core entity already carries a free-form 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 the custom_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 the custom_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.
Two rules keep stored data safe. 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 with validateCustomFields(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.