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::Featurefor migrations and console scripts; usePwbTenant::Featurefor web requests to maintain multi-tenant isolation. - Association strategy: Features link to properties through
prop_idorrealty_asset_idforeign keys, achieving tenancy indirectly rather than through awebsite_idcolumn. - UI integration: The admin interface manages features through
SearchFilterOptioncontrollers, while Liquid tags consume these definitions for frontend rendering. - Safe extension: Adding validations, scopes, or columns to
app/models/pwb/feature.rbautomatically 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →