Custom Fields
Brief
Custom Fields
Status: Draft | ID: SS-RP-2026-005
Overview
A system that allows workspace administrators to define custom fields on core object types (clients, properties, projects/designs, contacts, plants, invoices, etc.). Users can create fields of various data types — text, number, date, dropdown, multi-select, checkbox, URL, currency — and attach them to any supported object. Custom fields are workspace-scoped and governed by role-based accessibility rules: guest (hidden/view), member (view/edit), admin (full control).
Strategic Fit
Every landscape business tracks domain-specific data that no platform can anticipate: preferred stone vendor per client, last soil test date per property, HOA approval status per project, referral source per contact. Today users resort to notes fields or external spreadsheets. Custom fields turn SimplyScapes into an extensible data platform that adapts to each business's workflow — a key differentiator against rigid vertical SaaS competitors and a strong retention lever. This also lays the groundwork for filtered views, custom reports, and API integrations.
User Stories
-
As a workspace admin, I want to define custom fields for any supported object type so that my team can track business-specific data without leaving SimplyScapes.
-
As a workspace admin, I want to choose the data type for each custom field (text, number, date, dropdown, multi-select, checkbox, URL, currency) so that data entry is structured and consistent.
-
As a workspace admin, I want to set accessibility rules per field per role (hidden, view-only, editable) so that sensitive information is protected from guests and junior team members.
-
As a workspace member, I want to see and fill in custom fields on object detail pages so that I can capture the data my team needs.
-
As a workspace member, I want to filter and sort lists by custom field values so that I can find objects based on business-specific criteria.
-
As a guest user, I want to see only the custom fields my admin has made visible to me so that I have a clean, relevant experience.
-
As a workspace admin, I want to reorder, rename, and archive custom fields without losing historical data so that our field configuration evolves with our business.
-
As a user, I want custom field values to appear in PDF exports and proposals so that client-facing documents reflect our full data.
Research Report
Custom Fields — Research Report
ID: SS-RP-2026-005 | Date: 2026-03-15 | Status: complete Idea: Custom Fields Product: Platform Infrastructure → User & Workspace Management
TL;DR
Custom fields are table-stakes for any workspace-oriented SaaS product. Every major competitor (ClickUp, Monday, HubSpot, Jobber) offers them. The recommended implementation uses a JSONB column per object table with a separate field definition registry table — a hybrid approach that balances query flexibility (Hasura JSONB operators), schema simplicity, and performance. Phase 1 targets clients, contacts, properties, and projects (design_3d) — the four objects with the strongest user demand. Access control uses a three-tier permission model (hidden / view / edit) per role per field, enforced through Hasura permission rules and client-side rendering logic.
1. Schema Design — Recommendation: JSONB Hybrid
Patterns Evaluated
| Pattern | Pros | Cons | |---------|------|------| | EAV (Entity-Attribute-Value) | Fully normalized, easy to query individual fields | Explosion of rows, complex joins, poor Hasura DX | | Wide table (ALTER TABLE) | Fastest queries, native column types | Schema migrations per field, doesn't scale to many fields | | JSONB column | Flexible, Hasura-native filtering, no migrations per field | Weaker type enforcement, GIN index needed | | JSONB + definition table (hybrid) | Best of JSONB + structured metadata, Hasura-compatible | Slightly more complex writes |
Recommendation: JSONB Hybrid
Store custom field definitions in a dedicated custom_field_definition
table (workspace-scoped) and store values in a custom_fields JSONB
column added to each supported object table.
Why this wins:
- Hasura natively supports
_contains,_has_key, and path-based operators on JSONB columns — no computed fields needed for basic queries. - No DDL migrations when users create/delete fields — only data changes.
- Field definitions provide validation metadata, display order, and access rules without parsing JSONB.
- PostgreSQL GIN indexes on the JSONB column enable performant filtering.
- This is the pattern used by Linear, Attio, and ClickUp internally.
Schema Design
-- Field definitions (workspace-scoped)
CREATE TABLE custom_field_definition (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspace(id),
object_type text NOT NULL, -- 'client', 'contact', 'property', 'design_3d'
field_key text NOT NULL, -- machine-readable key (slug)
field_name text NOT NULL, -- display name
field_type text NOT NULL, -- 'text','number','date','dropdown','multi_select','checkbox','url','currency'
field_config jsonb NOT NULL DEFAULT '{}', -- options for dropdowns, currency symbol, validation rules
display_order int NOT NULL DEFAULT 0,
is_required boolean NOT NULL DEFAULT false,
is_archived boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(workspace_id, object_type, field_key)
);
-- Access control per field per role
CREATE TABLE custom_field_access (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
field_definition_id uuid NOT NULL REFERENCES custom_field_definition(id),
role text NOT NULL, -- 'admin', 'member', 'guest'
access_level text NOT NULL DEFAULT 'hidden', -- 'hidden', 'view', 'edit'
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(field_definition_id, role)
);
-- Values stored on each object table
ALTER TABLE client ADD COLUMN custom_fields jsonb NOT NULL DEFAULT '{}';
ALTER TABLE contact ADD COLUMN custom_fields jsonb NOT NULL DEFAULT '{}';
ALTER TABLE property ADD COLUMN custom_fields jsonb NOT NULL DEFAULT '{}';
ALTER TABLE design_3d ADD COLUMN custom_fields jsonb NOT NULL DEFAULT '{}';
JSONB Value Format
{
"referral_source": "Google Ads",
"hoa_approval": true,
"last_soil_test": "2026-01-15",
"preferred_stone_vendor": "Arizona Flagstone Co",
"project_budget": 45000
}
Keys correspond to field_key in the definition table. Values are stored
in their native JSON types (string, number, boolean, array for multi-select).
2. Competitive Landscape
Feature Matrix
| Feature | ClickUp | Monday | HubSpot | Notion | Jobber | Aspire | SimplyScapes (proposed) | |---------|---------|--------|---------|--------|--------|--------|---------------------------| | Custom text field | Yes | Yes | Yes | Yes | No | Yes | Yes | | Number field | Yes | Yes | Yes | Yes | No | Yes | Yes | | Date field | Yes | Yes | Yes | Yes | No | Yes | Yes | | Dropdown | Yes | Yes | Yes | Yes | No | Yes | Yes | | Multi-select | Yes | Yes | Yes | Yes | No | No | Yes | | Checkbox | Yes | Yes | Yes | Yes | No | No | Yes | | URL field | Yes | Yes | Yes | No | No | No | Yes | | Currency field | Yes | Yes | Yes | No | No | Yes | Yes | | Formula field | Yes | Yes | Yes | Yes | No | No | Phase 2 | | Relation/linked field | Yes | Yes | Yes | Yes | No | No | Phase 2 | | File attachment field | Yes | Yes | Yes | No | No | No | Phase 2 | | Field-level permissions | No | Yes | Yes | No | No | No | Yes | | Field on multiple objects | Yes | Yes | Yes | N/A | N/A | No | Yes | | Filter by custom field | Yes | Yes | Yes | Yes | No | Limited | Yes | | Sort by custom field | Yes | Yes | Yes | Yes | No | No | Yes | | Custom field in exports | Limited | Yes | Yes | No | No | Yes | Yes |
Key Insights
- Jobber and most landscape-vertical tools lack custom fields entirely. This is a clear differentiation opportunity in the landscaping vertical.
- Field-level permissions are rare — only Monday.com and HubSpot offer them. Most tools rely on workspace-level or object-level permissions. SimplyScapes implementing field-level access control is a differentiator.
- Formula and relation fields are table-stakes for power users but can safely be deferred to Phase 2.
- ClickUp's approach is the closest analog — workspace-scoped field definitions applicable to multiple object types with drag-and-drop reordering.
Landscape-Vertical Gap
No landscape SaaS tool surveyed (Jobber, Aspire, LMN, ServiceTitan, SingleOps) offers user-defined custom fields. They all use fixed schemas with "notes" fields as the escape hatch. This means:
- Custom fields are a significant differentiation opportunity.
- Landscaping businesses currently use external spreadsheets or CRM tools to track custom data — pulling them out of SimplyScapes.
3. Object Type Scoping
Phase 1 (Launch)
| Object Type | Table | Rationale |
|-------------|-------|-----------|
| Client | client | Businesses track client-specific data (referral source, account tier, contract type) |
| Contact | contact | Phone/email beyond standard fields, preferred communication channel, role/title specifics |
| Property | property | HOA restrictions, soil type, irrigation system age, lot characteristics |
| Project (Visual Design) | design_3d | Project budget, HOA approval status, installation crew, permit number |
Phase 2
| Object Type | Table | Rationale |
|-------------|-------|-----------|
| Aerial Design | design | Measurement-specific metadata |
| Plant | plant | Nursery-specific data, local availability notes |
| Invoice/Proposal | (future) | Line-item custom attributes |
| Organization | organization | Vendor/partner custom metadata |
Rationale
Phase 1 targets the CRM-adjacent objects that landscape businesses interact with daily and where the "notes field" workaround is most painful. Phase 2 extends to design and catalog objects where custom metadata is useful but less urgent.
4. Access Control Model
Permission Levels
| Level | Behavior | |-------|----------| | hidden | Field not rendered in UI, value not included in API response | | view | Field rendered as read-only, value included in API response | | edit | Field rendered as editable input, value can be created/updated via API |
Default Permissions by Role
| Role | Default | Can be changed to | |------|---------|-------------------| | admin | edit | — (always edit, cannot be restricted) | | member | edit | view, hidden | | guest | hidden | view (never edit) |
Design Decisions
- Admin always has edit access — admins define fields and must always be able to see and modify them. No self-lockout scenario.
- Guests can never edit — guests are external users (homeowners, subcontractors) who should only consume data, never modify workspace configuration or custom data.
- Members default to edit — the most common case is that team members need to fill in custom fields. Admins can restrict specific fields to view-only or hidden for members.
- Per-field granularity — permissions are set per field, not per group. This provides maximum flexibility without the complexity of field groups (which can come later).
Hasura Enforcement
- Hidden fields: Hasura permission rules exclude the
custom_fieldsJSONB paths for the guest role. A computed column or view can strip hidden fields server-side. - View-only fields: Enforced client-side (render as disabled) and server-side via a Hasura action that validates which fields the current user's role can write.
- Edit fields: Standard Hasura insert/update permissions on the
custom_fieldscolumn, with a validation action checking field-level write permissions.
5. Performance & Scalability
Indexing Strategy
-- GIN index for containment queries (@>, ?, ?|, ?& operators)
CREATE INDEX idx_client_custom_fields ON client USING gin (custom_fields);
CREATE INDEX idx_contact_custom_fields ON contact USING gin (custom_fields);
CREATE INDEX idx_property_custom_fields ON property USING gin (custom_fields);
CREATE INDEX idx_design_3d_custom_fields ON design_3d USING gin (custom_fields);
For frequently filtered fields, expression indexes can be added:
-- Expression index for a specific high-cardinality field
CREATE INDEX idx_client_cf_referral
ON client ((custom_fields->>'referral_source'));
Performance Estimates
| Scenario | Expected Performance |
|----------|---------------------|
| Simple key lookup (_has_key) | <5ms with GIN index |
| Value containment (_contains) | <10ms with GIN index up to 100k rows |
| Sort by JSONB path (->>'field') | Moderate — expression index recommended for hot fields |
| Full-text search within JSONB | Not recommended — use dedicated search |
Limits
| Dimension | Limit | Rationale | |-----------|-------|-----------| | Custom fields per object type per workspace | 100 | GIN index performance degrades beyond this | | Dropdown options per field | 200 | UI performance for select components | | Field name length | 100 chars | Display constraints | | Text field value length | 10,000 chars | Practical storage limit | | JSONB column size per row | ~1MB | PostgreSQL TOAST handles this transparently |
6. UX Patterns
Field Management
- Location: Workspace Settings → Custom Fields
- Organization: Tabs or dropdown to switch between object types
- Actions: Create, reorder (drag-and-drop), rename, archive, set permissions
- Pattern: Follows ClickUp and Monday.com — settings-based management with a flat list per object type
Object Detail View
- Placement: Custom fields section below standard fields, collapsible
- Rendering: Dynamic form based on field definitions and user role
- Inline editing: Click-to-edit pattern matching existing SimplyScapes UX
- Empty state: "No custom fields configured. [Set up custom fields →]" link for admins
List View Integration
- Column picker: Users can add custom field columns to list views
- Filtering: Custom fields appear in the filter builder with type-appropriate operators (contains, equals, greater than, etc.)
- Sorting: Single-column sort on custom field values
Component Mapping (shadcn)
| Field Type | View Component | Edit Component |
|------------|---------------|----------------|
| Text | <span> | <Input> |
| Number | <span> | <Input type="number"> |
| Date | <span> formatted | <DatePicker> (shadcn Calendar) |
| Dropdown | <Badge> | <Select> |
| Multi-select | <Badge> × N | <MultiSelect> (combobox) |
| Checkbox | <Checkbox> disabled | <Checkbox> |
| URL | <a> link | <Input type="url"> |
| Currency | <span> formatted | <Input> + currency prefix |
Recommendations
- Start with JSONB hybrid pattern — it's the pragmatic choice for Hasura + PostgreSQL and avoids the complexity of EAV.
- Launch with 4 object types (client, contact, property, design_3d) and 8 field types — this covers the most common use cases.
- Implement field-level access control from day one — it's a differentiator against landscape-vertical competitors and table-stakes competitors alike.
- Defer formula, relation, and file attachment fields to Phase 2 — they add significant complexity and are not blocking for the core use case.
- Use Hasura actions for write validation rather than trying to encode field-level write permissions in Hasura's native permission system, which operates at the column level, not the JSONB key level.
Competitive Analysis
Custom Fields — Competitive Analysis
Supporting document for: SS-RP-2026-005 | Date: 2026-03-15
Horizontal Platforms
ClickUp
- Custom fields are workspace-level, applicable to tasks/docs/lists
- 15+ field types including formula, relation, rollup, people, files
- No field-level permissions — visibility is tied to view/space access
- Drag-and-drop field reordering in task views
- Custom fields appear in list, board, and calendar views
- Filterable and sortable
Monday.com
- "Columns" serve as custom fields on boards
- 30+ column types including status, timeline, dependency, mirror
- Column-level permissions: restrict editing by role
- Most extensive field type library of any competitor
- Templates include pre-configured columns
HubSpot
- Custom properties on contacts, companies, deals, tickets
- Field-level permissions tied to user roles and teams
- Property groups for organization
- Calculated fields, dependent fields, conditional logic
- Strong API for custom property CRUD
- Up to 1,000 custom properties per object type
Notion
- Database properties (custom fields on database items)
- Types: text, number, select, multi-select, date, person, files, checkbox, URL, email, phone, formula, relation, rollup, created/edited time
- No field-level permissions — permissions are at page/database level
- Formula fields are powerful but have a learning curve
- Relations between databases are a key feature
Airtable
- Custom fields (columns) are the core of the product
- 20+ field types including attachment, barcode, button, rating
- Field-level permissions available on Enterprise plan
- Linked records (relations) are a core feature
- Views can show/hide fields without changing the base schema
Landscape/Construction Vertical
Jobber
- Fixed schema — no custom fields
- "Notes" text field on clients and properties
- Custom line items on quotes/invoices (name + price, no custom attributes)
- No filtering by custom data
Aspire (formerly Aspire Software)
- "Custom Fields" on contacts and properties (limited to 10 per object)
- Text, number, date, dropdown types only
- No multi-select, no checkbox, no URL
- No field-level permissions
- Not filterable in list views
LMN (Landscape Management Network)
- No custom fields
- Structured estimating templates with fixed columns
- Job costing uses fixed categories
ServiceTitan
- "Custom Fields" on customers and jobs
- Text, number, date, dropdown
- Available on higher-tier plans only
- Basic filtering support
- No access control on fields
SingleOps
- "Tags" system (effectively multi-select custom fields)
- No typed custom fields (no number, date, etc.)
- Tags are filterable
Summary
| Capability | ClickUp | Monday | HubSpot | Notion | Airtable | Jobber | Aspire | ServiceTitan | |-----------|---------|--------|---------|--------|----------|--------|--------|-------------| | Custom fields exist | Yes | Yes | Yes | Yes | Yes | No | Limited | Limited | | Field types | 15+ | 30+ | 20+ | 15+ | 20+ | — | 4 | 4 | | Field-level permissions | No | Yes | Yes | No | Enterprise | — | No | No | | Custom field filtering | Yes | Yes | Yes | Yes | Yes | — | No | Basic | | Formula fields | Yes | Yes | Yes | Yes | Yes | — | No | No | | Relation fields | Yes | Yes | Yes | Yes | Yes | — | No | No | | API access | Yes | Yes | Yes | Yes | Yes | — | No | Limited | | Max fields | ~100 | Unlimited | 1000 | Unlimited | ~500 | — | 10 | ~20 |
Opportunity
The landscape vertical has a massive gap. No competitor offers comprehensive custom fields with access control. SimplyScapes can leapfrog vertical competitors by shipping a horizontal-grade custom fields system (comparable to ClickUp/Monday) while remaining focused on the landscape professional workflow.
Schema Design Patterns
Custom Fields — Schema Design Patterns
Supporting document for: SS-RP-2026-005 | Date: 2026-03-15
Pattern Comparison
1. Entity-Attribute-Value (EAV)
CREATE TABLE custom_field_value (
id uuid PRIMARY KEY,
entity_type text NOT NULL, -- 'client', 'contact', etc.
entity_id uuid NOT NULL, -- FK to the object
field_key text NOT NULL, -- field identifier
value_text text,
value_num numeric,
value_bool boolean,
value_date timestamptz,
value_json jsonb, -- for arrays (multi-select)
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
Pros:
- Fully normalized, one row per value
- Easy to query a single field's values across all entities
- Type-specific columns avoid casting issues
Cons:
- Loading all custom fields for one object = N queries or a pivot
- Hasura relationships become complex (array relationship per entity type)
- Write amplification: creating an object with 10 custom fields = 10 inserts
- Joins scale poorly: 100k objects × 20 fields = 2M rows in value table
Verdict: Avoid for Hasura-based systems. The GraphQL DX is poor and the query patterns don't map well to Hasura's subscription model.
2. Wide Table (Dynamic ALTER TABLE)
-- For each new field, run:
ALTER TABLE client ADD COLUMN cf_referral_source text;
ALTER TABLE client ADD COLUMN cf_project_budget numeric;
Pros:
- Fastest possible queries — native PostgreSQL column access
- Full type enforcement
- Standard Hasura column permissions
- Works with existing indexes, sorts, and filters
Cons:
- Requires DDL per field creation/deletion — needs admin DB access
- Hasura metadata must be reloaded after each ALTER TABLE
- Column limit (~1,600 per table in PostgreSQL) is a hard ceiling
- Schema drift between workspaces if fields are workspace-scoped
- Cannot support workspace-scoped fields without prefixing or separate tables
Verdict: Not viable for workspace-scoped custom fields. Would require per-workspace tables or a shared column namespace with complex naming conventions.
3. JSONB Column
ALTER TABLE client ADD COLUMN custom_fields jsonb NOT NULL DEFAULT '{}';
Pros:
- No DDL per field — purely data-driven
- Hasura natively supports JSONB operators (
_contains,_has_key,_cast) - Single column to manage permissions on
- Flexible — any field type maps to JSON types
- TOAST handles large values transparently
Cons:
- No database-level type enforcement per key
- GIN indexes are required for query performance
- Sorting by JSONB path requires expression indexes for performance
- Hasura subscriptions on JSONB changes trigger on any key change
Verdict: Strong choice. The main weakness (type enforcement) is mitigated by application-level validation via Hasura actions.
4. JSONB Hybrid (Recommended)
Combines a definition table for metadata with a JSONB column for values.
custom_field_definition (table)
├── defines field types, validation, display order
├── defines access rules per role
└── workspace-scoped
client.custom_fields (JSONB column)
└── stores values keyed by field_key
Pros:
- All benefits of JSONB pattern
- Field definitions provide validation metadata without parsing JSONB
- Access rules stored relationally (easy to query and enforce)
- Display order, archival, and field configuration are first-class
- Clean GraphQL API via Hasura: query definitions + values separately
Cons:
- Slightly more complex write path (validate against definitions)
- Two sources of truth that must stay in sync (definition exists → values valid)
Mitigation:
- Hasura action validates writes against definitions
- Orphaned values (definition deleted) are harmless — just ignored by UI
- Archiving (not deleting) definitions preserves historical data
Hasura Integration Details
Reading Custom Fields
query GetClientWithCustomFields($id: uuid!) {
client_by_pk(id: $id) {
id
client_name
custom_fields # Returns full JSONB object
}
}
Filtering by Custom Field Value
query FilterClientsByReferral {
client(where: {
custom_fields: { _contains: { referral_source: "Google Ads" } }
}) {
id
client_name
custom_fields
}
}
Checking if a Custom Field Exists
query ClientsWithBudgetField {
client(where: {
custom_fields: { _has_key: "project_budget" }
}) {
id
client_name
}
}
Sorting by Custom Field (Requires Computed Field or View)
Hasura does not natively support ORDER BY jsonb_column->>'key'.
Options:
- Computed field returning the extracted value (recommended for hot fields)
- Database view with extracted columns
- Client-side sorting for low-volume lists
Recommendation: Client-side sorting for Phase 1, add computed fields for high-use sort columns in Phase 2 based on usage telemetry.
Indexing Deep Dive
GIN Index
CREATE INDEX idx_client_custom_fields ON client USING gin (custom_fields);
Supports:
@>(contains):custom_fields @> '{"referral_source": "Google Ads"}'?(has key):custom_fields ? 'referral_source'?|(has any key):custom_fields ?| array['key1', 'key2']?&(has all keys):custom_fields ?& array['key1', 'key2']
Performance: GIN indexes are excellent for equality and containment queries. Expected <10ms for tables up to 500k rows.
Expression Index (for sort/range queries)
CREATE INDEX idx_client_cf_budget
ON client ((custom_fields->>'project_budget')::numeric);
Supports ORDER BY and range comparisons on a specific JSONB key.
Create these on-demand based on usage telemetry, not upfront.
Partial Index (for filtered queries)
CREATE INDEX idx_client_cf_hoa_approved
ON client ((custom_fields->>'hoa_approval'))
WHERE custom_fields ? 'hoa_approval';
Efficient for boolean or low-cardinality fields that are frequently filtered.
Migration Strategy
Adding a New Custom Field
- Insert row into
custom_field_definition— no schema migration - UI picks up new field on next load via definition query
Changing Field Type
- Update
field_typein definition - Existing values remain as-is (JSON is type-flexible)
- Application validates new inputs against new type
- Optional: background job to coerce existing values (e.g., string "42" → number 42)
Archiving a Field
- Set
is_archived = trueon definition - UI stops rendering the field
- Values remain in JSONB — no data loss
- Can be unarchived later
Deleting a Field (Permanent)
- Delete definition row
- Optionally: background job to remove key from all JSONB objects
- Values without matching definitions are harmless (ignored by UI)