DEV Community

Cover image for ActiveFields: Multi-tenancy
Kirill Usanov
Kirill Usanov

Posted on

ActiveFields: Multi-tenancy

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

Implementation Details

ActiveFields scoping works as follows:

  1. Scope Method: The method you specify (e.g., :tenant_id) is called on each record to determine its scope
  2. String Conversion: The scope value is automatically converted to a string for comparison and available as active_fields_scope
  3. Field Matching: When querying active_fields, ActiveFields matches:
    • Global fields (scope: nil) => Always included
    • Scoped fields (scope: "tenant_1") => Only if record scope matches
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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_method to has_active_fields
  • Users create fields through your UI; your application code could explicitly set the scope based on their tenant
  • Use scope: nil for 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)