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

TypeInput UIStorage
textText inputstring (max 1,000 chars)
numberNumber inputnumber
dateDate pickerISO date string
booleanToggle switchboolean
selectDropdownstring (one of options)
multi_selectMulti-checkboxstring[]
urlURL inputURL string
emailEmail inputemail string

Managing fields

Go to Settings → Custom Fields (admin or owner role required). Four tabs: Contact, Deal, Project, Lead.

Creating a field

  1. Click Add field
  2. Enter a Label (e.g. “Contract Type”) — the Field Key auto-derives as snake_case and must be unique per entity type in the org
  3. Choose a Field Type
  4. For select / multi_select, enter allowed options one per line
  5. Optionally set Required, Placeholder, Description
  6. Click Add field — appears immediately on entity Add/Edit dialogs
Note: field_key and field_type cannot 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

ItemDetail
Server actionsgetCustomFieldDefinitions, createCustomFieldDefinition, updateCustomFieldDefinition, deleteCustomFieldDefinition, reorderCustomFieldDefinitions
Action fileactions/custom-fields.ts
Feature flagscrm:custom-fields (Pro), projects:custom-fields (Pro)
Key migrations00041_custom_fields.sql (definitions + metadata columns), 00057_custom_fields_company_entity.sql (GIN index)
Audit actionscustom_field.created, custom_field.updated, custom_field.deleted