RFC: Hybrid Custom Fields on Core Tables
RFC: Hybrid Custom Fields on Core Tables
- Authors: Prathik Shetty(@pshettydev)
- Date: 2025-11-21
- Status: Accepted
Abstract
This RFC proposes a hybrid approach for enabling dynamic, tenant-defined custom fields on core ATS tables (companies, positions, contacts, candidates, etc.) by combining JSONB storage with the existing Schema-Driven Table Framework (SDTF).
The system leverages:
- JSONB columns for storing custom field values on each core table
- SDTF schema for managing custom field definitions, UI configuration, and validation rules
- Application-layer validation with optional database-level type enforcement via generated columns
Motivation
Business Requirements
- Flexibility: Tenants need to define custom fields specific to their business processes without waiting for engineering deployments (e.g., "Customer Tier", "Deal Size", "Priority Score")
- Performance: Custom field operations must not degrade core table query performance
- User Experience: Custom fields should appear seamlessly alongside native fields in the UI
- Type Safety: Custom fields must support various data types (text, number, date, select, relations) with proper validation
Technical Context
Implementing the entire dynamic tables without defining any core tables in the DB (as per our previous analysis in The EAV Approach and JSONB Approach) had many benefits, however, it came with its own share of drawbacks. Which included:
- Complexity: Managing custom fields across multiple tables and ensuring consistency can be challenging
- Migration: Migrating the entire backend as per the new schema and also migrating the data from the present DB to the new schema can be error-prone
- Time Constraint: The most important issue of all, given the timeline, it is extremely difficult to ensure that the implementation is completed within the specified timeframe.
Core Architecture
Why Not Pure Dynamic Tables?
While we have dynamic table capabilities, core tables (companies, positions, contacts) have:
- Complex existing relationships and foreign key constraints
- Heavy indexing and query optimization requirements
- Critical business logic and validation rules
- Integration with external systems expecting stable schemas (like migration with Airtable)
A hybrid approach preserves these advantages while adding customization capabilities.
Approach
JSONB Column Per Core Table
Add a custom_fields JSONB column to each core table that supports customization.
Schema Example:
Field Structure:
Strengths:
- ✅ Performance: Single-row fetch, no joins required
- ✅ Indexing: GIN indexes enable fast JSON path queries
- ✅ Sparse Storage: Null if no custom fields defined
- ✅ Postgres-Native: Leverages JSONB operators and functions
- ✅ Simple Queries: Standard WHERE clauses with JSONB operators
Weaknesses:
- ❌ Schema Migration: One-time migration needed per core table
- ❌ Size Limits: JSONB practical limit (~100MB per record)
- ❌ Type Enforcement: Requires application-layer validation
However, these weaknesses have already been mitigated or can be managed. For example, type enforcement is already handled at the application layer through @pnats/core, which utilizes Zod schemas for validation.
Query Examples:
Proposal
Adopt JSONB-backed custom fields as the primary implementation with a clear path to promote high-traffic fields to generated columns based on production profiling.
When a user creates a new custom field for a core table, the field definition is registered in the SDTF schema layer, which serves as the single source of truth for all field metadata.
However, the system employs a lazy population strategy: the custom field is not automatically initialized in the custom_fields JSONB column of existing records. Instead, custom field values are only written to a record when that record is explicitly updated or when a value is assigned to the new field.
Benefits of this approach:
- Storage efficiency: Avoids bloating existing records with null/empty fields
- Performance: Eliminates the need for expensive bulk updates when fields are added
- Sparse data handling: Records naturally contain only the custom fields that have been populated
Implementation Strategy
Core Schema Changes
Add custom_fields JSONB column to all core tables that require customization:
Tables to include:
- ✅ Company
- ✅ Position
- ✅ Contact
- ✅ Candidate
- ✅ CandidateSubmission
- ✅ Opportunity
- ✅ Interview
- ✅ Agreement
- ✅ Lead (optional)
Tables to exclude (system tables):
- ❌ User
- ❌ Tenant
- ❌ Permission
- ❌ Role
- ❌ AuditLog
SDTF Schema Extension
Extend TableSchema to support core table customization:
Schema Creation Pattern:
For each core table, create a corresponding TableSchema entry that includes:
- All native fields (marked with
is_custom_field = false) - Custom fields defined by tenant (marked with
is_custom_field = true)
Example SDTF schema for companies with custom fields:
Validation Layer
Implement multi-tier validation strategy:
Tier 1: Application-Layer Validation (Primary)
Store validation rules in TableField.validation JSONB:
Tier 2: Database Type Hints (Optional - Future Enhancement)
For critical custom fields that require database-level type safety, promote to generated columns:
If done correctly, we can have scripts that automatically generate these columns and indexes based on the validation rules. _ :heart_eyes: _
API Layer Design
Request Format (Create/Update):
Response Format:
Query Parameters:
Backend Translation:
Key Design Decisions (v.v.v. important)
1. Field Addressing
Use deterministic, immutable field IDs to survive renames:
Rationale:
- Field display name can change without affecting data
- No collision risk across tenants (UUID guaranteed unique)
- Clear distinction from native field names
2. Type Coercion
Store values in their natural JSON types within JSONB:
Type Handling:
3. Relational Custom Fields
Support linking custom fields to other records:
SDTF Field Definition:
Query with Relation Resolution:
4. Indexing Strategy
Base Index (All Tables):
Specific Field Indexes (As Needed):
When to Add Specific Indexes:
These are a few important factors to consider when deciding whether to add specific indexes:
- Field appears in >10% of queries
- Filter/sort performance is measurably slow
- Query planner shows sequential scans
5. Performance Optimization
Optimization Path:
Example Promotion:
Data Integrity Considerations
Handling Type Changes
When a field type changes (e.g., TEXT → NUMBER):
Handling Null vs Undefined
Security & Permissions
Field-Level Permissions
Extend existing permission system to support custom field access control:
Permission Check in API:
Error Handling
Validation Errors
Type Coercion Errors
Future Enhancements
Conditional Field Visibility
Show/hide fields based on other field values:
Field Templates
Pre-configured field sets for common use cases:
Bulk Operations
Efficient bulk updates for custom fields:
Custom Field Analytics
Aggregations and reporting on custom fields:
Field Change Notifications
Webhook/event system for custom field changes:
References
- EAV Approach RFC - Original EAV pattern analysis
- EAV to JSONB RFC - Migration path to JSONB
- SDTF Creation Guide - SDTF schema documentation
- PostgreSQL JSONB Documentation
- PostgreSQL GIN Indexes
- Prisma JSON Fields
Conclusion
The hybrid approach leveraging JSONB storage for custom fields on core tables provides the optimal balance of flexibility, performance, and maintainability, at least for now.
We achieve an Airtable-like customization experience without sacrificing the robustness of our core data model, which was the whole point of taking this approach. This approach aligns with our previous technical analysis, leverages existing infrastructure (SDTF), and provides a clear evolution path through generated columns as usage patterns emerge.
With careful monitoring and the ability to promote hot fields to generated columns, this architecture will serve our dynamic field requirements for the foreseeable future, fingers crossed.