Extending Property Features with the Feature Model in PropertyWebBuilder

The Feature model in PropertyWebBuilder provides a dual-class architecture (Pwb::Feature for global operations and PwbTenant::Feature for tenant-scoped requests) that stores property amenities through the pwb_features table and links to properties via optional polymorphic-style associations.

In the open-source real estate CMS PropertyWebBuilder (etewiah/property_web_builder), the Feature model represents individual property amenities such as pools, gardens, or garages. Understanding how to extend this model is essential when adding custom validations, new database columns, or specialized query scopes while maintaining multi-tenant isolation.

Understanding the Feature Model Architecture

The repository implements two variants of the Feature class to handle different execution contexts: a global base class for console work and a tenant-scoped wrapper for web requests.

Global Definition (Pwb::Feature)

The base definition lives in app/models/pwb/feature.rb and inherits directly from ActiveRecord::Base:

class Feature < ActiveRecord::Base
  self.table_name = 'pwb_features'

  belongs_to :prop,                 optional: true, class_name: 'Pwb::Prop'
  belongs_to :realty_asset,         optional: true, class_name: 'Pwb::RealtyAsset'
  belongs_to :feature_field_key,    optional: true,
             class_name: 'Pwb::FieldKey',
             foreign_key: :feature_key, primary_key: :global_key
end

This class maps to the pwb_features table and defines optional associations to both Prop (standard properties) and RealtyAsset (listing assets). Unlike other tenant-scoped models in the codebase, Pwb::Feature does not use acts_as_tenant because it lacks a website_id column.

Tenant-Scoped Wrapper (PwbTenant::Feature)

For web requests requiring automatic multi-tenant isolation, the repository provides a thin wrapper in app/models/pwb_tenant/feature.rb:

class Feature < Pwb::Feature
  # No acts_as_tenant; tenancy is inherited via the parent Prop/RealtyAsset.

end

Why two classes? Since Pwb::Feature cannot be directly scoped by acts_as_tenant, tenancy enforcement occurs through its associations. When you query PwbTenant::Feature.where(...), the current tenant's website context automatically filters features through their linked Prop or RealtyAsset records.

Associating Features with Properties

A Feature connects to a property through either the prop_id or realty_asset_id foreign key. Both fields are optional, allowing features to exist independently until assigned.

To create a feature for a standard property:

property = PwbTenant::Prop.find_by(address: '123 Main St')
feature = PwbTenant::Feature.create!(
  feature_key: 'pool',
  prop: property
)

For listings tied to a RealtyAsset rather than a Prop:

asset = PwbTenant::RealtyAsset.find(uuid)
PwbTenant::Feature.create!(feature_key: 'garden', realty_asset: asset)

Integrating Features with the Admin UI and Frontend

The repository connects features to the user interface through two primary mechanisms: search filter management and Liquid tag rendering.

Search Filter Management

The admin interface for feature configuration resides in app/controllers/site_admin/search_filters/features_controller.rb. This controller operates on Pwb::SearchFilterOption objects rather than direct Feature instances. Each SearchFilterOption maps to a feature_key stored in the Feature model, managing UI-level metadata such as visibility, display labels, and icons.

Liquid Tag Rendering

The {% featured_properties %} Liquid tag (defined in app/lib/pwb/liquid_tags/featured_properties_tag.rb) queries properties using the current website context. When this tag renders property grids, it automatically respects the visible and show_in_search flags defined in the associated SearchFilterOption records, ensuring only approved features appear in frontend filters.

Extending the Feature Model

Because Pwb::Feature inherits from ActiveRecord::Base, you can safely extend it with custom business logic. Since PwbTenant::Feature inherits from Pwb::Feature without overriding behavior, additions to the base class immediately become available in tenant contexts.

Add custom validations and helper methods:


# app/models/pwb/feature.rb

class Feature < ActiveRecord::Base
  # ... existing associations ...

  validates :feature_key, presence: true, 
            uniqueness: { scope: [:prop_id, :realty_asset_id] }

  def display_name
    I18n.t("features.#{feature_key}", default: feature_key.titleize)
  end

  scope :highlighted, -> { where(highlighted: true) }
end

Database modifications require standard Rails migrations targeting the pwb_features table:

class AddHighlightedToFeatures < ActiveRecord::Migration[6.1]
  def change
    add_column :pwb_features, :highlighted, :boolean, default: false
  end
end

Common Workflows and Code Examples

The following patterns demonstrate typical extension scenarios:

Creating global feature seeds:


# db/seeds.rb

%w[pool garage garden balcony].each do |key|
  Pwb::Feature.find_or_create_by!(feature_key: key)
end

Querying features within a tenant scope:


# In a controller or view

featured = PwbTenant::Feature
            .joins(:prop)
            .where(props: { website_id: @current_website.id })
            .highlighted

Adding features via console:


# For administrative scripts or data migration

property = Pwb::Prop.find(123)
Pwb::Feature.create!(feature_key: 'solar_panels', prop: property)

Summary

  • Dual-class architecture: Use Pwb::Feature for migrations and console scripts; use PwbTenant::Feature for web requests to maintain multi-tenant isolation.
  • Association strategy: Features link to properties through prop_id or realty_asset_id foreign keys, achieving tenancy indirectly rather than through a website_id column.
  • UI integration: The admin interface manages features through SearchFilterOption controllers, while Liquid tags consume these definitions for frontend rendering.
  • Safe extension: Adding validations, scopes, or columns to app/models/pwb/feature.rb automatically propagates to tenant contexts due to simple inheritance.

Frequently Asked Questions

What is the difference between Pwb::Feature and PwbTenant::Feature?

Pwb::Feature is the global ActiveRecord base class used for database migrations, seed scripts, and cross-tenant operations. PwbTenant::Feature is a thin subclass used exclusively within web requests; it inherits all behavior from the base class but operates within the current tenant's website context through its associations with Prop or RealtyAsset.

How does multi-tenancy work if the Feature model lacks a website_id column?

Tenancy is achieved through indirect scoping. Since every Feature belongs to either a Prop or RealtyAsset, and those parent records do belong to a specific Website, querying through PwbTenant::Feature automatically respects tenant boundaries when you join or filter through the parent associations.

What is the relationship between Feature and SearchFilterOption?

SearchFilterOption stores UI-specific metadata (labels, icons, visibility flags) for each feature_key, while Feature stores the actual assignment of that amenity to a specific property. The admin UI manages SearchFilterOption records, but the underlying property data uses the Feature model with matching feature_key values.

Can I add custom columns to the features table safely?

Yes. Create a standard Rails migration targeting the pwb_features table (as defined in app/models/pwb/feature.rb via self.table_name). New columns are accessible from both Pwb::Feature and PwbTenant::Feature immediately after migration, and you can add validations or scopes in the base model file to enforce business rules.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →