ActiveFields is designed to give system users the ability to add custom fields to entities without requiring database access or code changes. In multi-tenant applications, the scoping feature ensures users can only add fields for their own tenant, maintaining proper data isolation.
The Core Concept
If you're a developer building a multi-tenant system and need to store tenant-specific fields, you have straightforward options: add columns to your tables or use JSONB columns. These approaches work fine when you control the schema and know the fields at development time.
However, ActiveFields solves a different problem: enabling end users (who don't have database access or source code) to customize their own entities. These users need a way to:
- Add custom fields to system entities through a UI
- Define field types, validation rules, and constraints
- Search and filter by these custom fields
- In multi-tenant systems, only add fields for their own tenant
The Multi-Tenant Challenge
Consider a SaaS platform where:
- Tenant A wants to add custom fields like "employee_id" and "department_code" to their User records
- Tenant B wants different fields like "client_number" and "project_code" for the same entity type
- System administrators may define common fields like "notes" available to all tenants
Without scoping, tenant isolation becomes a problem:
- A user from Tenant A could accidentally create fields visible to Tenant B
- There is no way to restrict field creation to a specific tenant
- Users need explicit database-level isolation for their customizations
ActiveFields scoping ensures that when users create custom fields those fields are automatically isolated to their tenant.
How Scoping Works
From a developer perspective, enabling scoping is simple:
class User < ApplicationRecord
has_active_fields scope_method: :tenant_id
end
When your application users create fields (e.g., via an admin panel or API), your application code must explicitly set the scope based on the current user tenant:
# Global field
ActiveFields::Field::Text.create!(
name: "note",
customizable_type: "User",
scope: nil,
)
# User from Tenant A creates a field
ActiveFields::Field::Integer.create!(
name: "employee_id",
customizable_type: "User",
scope: user_1.tenant_id, # Explicitly set by your application code
)
# User from Tenant B creates a different field
ActiveFields::Field::Text.create!(
name: "client_number",
customizable_type: "User",
scope: user_2.tenant_id, # Explicitly set to Tenant B scope
)
When records are accessed, ActiveFields automatically filters which fields are available:
user_1 = User.find_by(tenant_id: "tenant_1")
user_1.active_fields
# Returns: [note (global), employee_id (scoped to tenant_1)]
# Tenant 1 user sees their custom field
user_2 = User.find_by(tenant_id: "tenant_2")
user_2.active_fields
# Returns: [note (global), client_number (scoped to tenant_2)]
# Tenant 2 user sees their different custom field
The filtering happens automatically. Users from different tenants see different sets of custom fields, and system administrators can still define global fields accessible to everyone.
Real-World Scenarios
1. Multi-Tenant SaaS with User Customization
Users from different tenants customize the same entity type differently:
class Product < ApplicationRecord
belongs_to :tenant
has_active_fields scope_method: :tenant_id
end
When tenant admins create fields through your UI:
- Tenant A (e-commerce company) adds fields like "sku" and "weight_kg"
- Tenant B (SaaS provider) adds fields like "license_key" and "subscription_tier"
- System admin creates global fields like "is_active" available to all
Each tenant only sees and can manage their own custom fields, plus any global fields defined by system administrators.
2. Department-Based Customization
In an organization-wide system, different departments might need different custom fields:
class Employee < ApplicationRecord
belongs_to :department
has_active_fields scope_method: :department_id
end
Department administrators can:
- HR department adds fields like "performance_review_date"
- Engineering department adds fields like "programming_languages"
Users from each department only see fields relevant to their scope.
3. Client-Specific Configuration
In a client management system, different clients may need different custom fields:
class Project < ApplicationRecord
belongs_to :client
has_active_fields scope_method: :client_id
end
Client administrators can add fields specific to their organization without affecting other clients' configurations.
Implementation in Your UI
ActiveFields provides a scaffold generator that creates base CRUD functionality for managing fields. You can generate it with:
bin/rails generate active_fields:scaffold
This creates controllers, routes, and views for managing Active Fields. The generated controller includes scope in the permitted attributes for field creation. You'll need to customize it to automatically set the scope based on the current user's tenant when creating fields through your UI.
The key point: your application should always set the scope for custom fields according to the user's tenant (for example, using current_user.tenant_id). This guarantees that users can only view and manage fields that belong to their own tenant, maintaining proper data isolation and security.
Querying with Scopes
When users search or filter entities using their custom fields, scoping ensures they only search within their tenant fields:
# Search using tenant-scoped fields
current_user = User.find_by(tenant_id: "tenant_1")
User.where_active_fields(
[
{ name: "employee_id", operator: "=", value: 1337 },
],
scope: current_user.tenant_id,
)
Or when displaying available fields to users in your UI:
# Show only fields available to the current tenant
def available_fields_for_current_tenant
User.active_fields(scope: current_user.tenant_id)
# Returns: global fields + fields scoped to current_user tenant
end
Computed Scopes
For more complex scenarios where scope depends on multiple attributes:
class User < ApplicationRecord
belongs_to :tenant
belongs_to :department
has_active_fields scope_method: :tenant_and_department_scope
def tenant_and_department_scope
"#{tenant_id}-#{department_id}"
end
end
This allows users to create fields scoped to specific tenant-department combinations, enabling fine-grained customization control.
Handling Scope Changes
When a record scope changes (e.g., an employee moves departments), custom field values that are no longer available should be cleaned up:
class User < ApplicationRecord
has_active_fields scope_method: :tenant_id
after_update :clear_unavailable_active_values, if: :saved_change_to_tenant_id?
end
The clear_unavailable_active_values method:
- Identifies active values referencing fields no longer available after scope change
- Destroys those orphaned values
- Maintains data consistency
For computed scopes, detect changes manually:
class User < ApplicationRecord
has_active_fields scope_method: :composite_scope
def composite_scope
"#{tenant_id}-#{region_id}"
end
after_update :clear_unavailable_active_values, if: :composite_scope_changed?
private
def composite_scope_changed?
saved_change_to_tenant_id? || saved_change_to_region_id?
end
end
Global vs. Scoped Fields
Global Fields (created by system administrators):
- Available to all tenants
- Useful for system-wide metadata like "notes" or "status"
- Defined with
scope: nil - Typically created programmatically or through a super-admin interface
Scoped Fields (created by tenant users):
- Only visible within the tenant scope
- Tenant-specific business logic fields
- Defined with
scope: tenant_id - Created by users through your customization UI
Example:
# Global - system admin creates this for all tenants
ActiveFields::Field::Boolean.create!(
name: "is_archived",
customizable_type: "Document",
scope: nil, # Available to all tenants
)
# Scoped - healthcare tenant user creates this
ActiveFields::Field::Date.create!(
name: "hipaa_compliance_date",
customizable_type: "Document",
scope: healthcare_tenant.id, # Only for this tenant
)
# Scoped - finance tenant user creates this
ActiveFields::Field::Text.create!(
name: "sox_category",
customizable_type: "Document",
scope: finance_tenant.id, # Only for this tenant
)
Implementation Details
ActiveFields scoping works as follows:
-
Scope Method: The method you specify (e.g.,
:tenant_id) is called on each record to determine its scope -
String Conversion: The scope value is automatically converted to a string for comparison and available as
active_fields_scope -
Field Matching: When querying
active_fields, ActiveFields matches:- Global fields (
scope: nil) => Always included - Scoped fields (
scope: "tenant_1") => Only if record scope matches
- Global fields (
-
Automatic Filtering: All ActiveFields methods (
active_fields,where_active_fields,initialize_active_values, etc.) respect scoping automatically
Migration Strategy
If you're upgrading from ActiveFields version <3 and want to use scoping:
1. Run the install generator: This will create a migration to add the scope column to the active fields table. Then run this migration.
bin/rails generate active_fields:install
bin/rails db:migrate
Note: Existing fields remain global. The scope column defaults to nil, so all existing fields automatically become global fields (available to all tenants).
2. Enable scoping gradually: Add scope_method to models as needed.
class User < ApplicationRecord
has_active_fields scope_method: :tenant_id
end
When users create new fields, explicitly set the scope based on their tenant (see implementation examples above).
Summary
ActiveFields scoping enables multi-tenant entities customization by ensuring that when users create custom fields with specified scope, those fields are isolated to this scope.
This provides:
- User customization: End users can add fields without database access
- Tenant isolation: Fields created by users could be explicitly scoped to their tenant by your application code
- Data isolation: Each tenant only sees and can use their own custom fields
- Global fields: System administrators can still define fields available to all tenants
- Search integration: Scoping works seamlessly with search functionality
Key Points:
- Enable scoping by passing
scope_methodtohas_active_fields - Users create fields through your UI; your application code could explicitly set the scope based on their tenant
- Use
scope: nilfor global fields (system-defined, available to all) - Use
scope: "tenant_1"for scoped fields (user-defined, tenant-specific) - Handle scope changes with
clear_unavailable_active_values - Supports computed scopes for complex scenarios
If you need to enable per-tenant entities fields customization in your multi-tenant Rails application, ActiveFields provides custom fields with built-in scoping that ensures proper tenant isolation.
Top comments (0)