Features
Custom fields
Custom fields let org admins define their own data schema for any entity — Contacts, Deals, Projects, Leads, and Companies — without touching migrations. Values are stored in a metadata JSONB column on each entity table. Pro feature (crm:custom-fields, projects:custom-fields).
Architecture
custom_field_definitions table (schema registry)
↓ defines structure and validation rules for
metadata JSONB column (values, stored on each entity row)Why hybrid JSONB? Definitions live in a queryable relational table — enumerate, filter, and validate them easily. Values live in JSONB on the entity row — no JOIN needed when loading a contact or deal; values travel with the record. This is the industry-standard approach (used by HubSpot, Pipedrive, Linear).
Supported field types
| Type | Input UI | Storage |
|---|---|---|
| text | Text input | string (max 1,000 chars) |
| number | Number input | number |
| date | Date picker | ISO date string |
| boolean | Toggle switch | boolean |
| select | Dropdown | string (one of options) |
| multi_select | Multi-checkbox | string[] |
| url | URL input | URL string |
| Email input | email string |
Managing fields
Go to Settings → Custom Fields (admin or owner role required). Four tabs: Contact, Deal, Project, Lead.
Creating a field
- Click Add field
- Enter a Label (e.g. “Contract Type”) — the Field Key auto-derives as
snake_caseand must be unique per entity type in the org - Choose a Field Type
- For
select/multi_select, enter allowed options one per line - Optionally set Required, Placeholder, Description
- Click Add field — appears immediately on entity Add/Edit dialogs
Note:field_keyandfield_typecannot be changed after creation. To change a type or key, delete the old field and create a new one. Existing metadata values are preserved — the definition is soft-deleted (is_active = false).
Editing a field
Click the ⋯ menu → Edit field. Editable: label, options (select types), required, placeholder, description.
Reordering fields
Drag the ⠿ handle to reorder. Order is saved via reorderCustomFieldDefinitions.
How values are stored
-- On each entity row
metadata jsonb NOT NULL DEFAULT '{}'
-- Example stored value on a contact row
{
"contract_type": "retainer",
"preferred_channel": "email",
"renewal_score": 8
}The custom_field_definitions table has a GIN index on entity_type for fast field lookups. Entity tables have GIN indexes on the metadata column for JSONB queries.
Technical reference
| Item | Detail |
|---|---|
| Server actions | getCustomFieldDefinitions, createCustomFieldDefinition, updateCustomFieldDefinition, deleteCustomFieldDefinition, reorderCustomFieldDefinitions |
| Action file | actions/custom-fields.ts |
| Feature flags | crm:custom-fields (Pro), projects:custom-fields (Pro) |
| Key migrations | 00041_custom_fields.sql (definitions + metadata columns), 00057_custom_fields_company_entity.sql (GIN index) |
| Audit actions | custom_field.created, custom_field.updated, custom_field.deleted |