# Extending Property Features with the Feature Model in PropertyWebBuilder

> Learn how to extend property features with the Feature model in PropertyWebBuilder. Discover its dual-class architecture and polymorphic associations for managing property amenities effectively.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: deep-dive
- Published: 2026-03-01

---

**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`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/feature.rb) and inherits directly from `ActiveRecord::Base`:

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb_tenant/feature.rb):

```ruby
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:

```ruby
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`:

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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:

```ruby

# 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:

```ruby
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:**

```ruby

# 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:**

```ruby

# In a controller or view

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

```

**Adding features via console:**

```ruby

# 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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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.