# How Multi-Tenancy with acts_as_tenant Works in PropertyWebBuilder

> Learn how PropertyWebBuilder uses acts_as_tenant for automatic row-level tenant isolation, securing your data based on request headers or domains and scoping all database queries.

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

---

**PropertyWebBuilder uses the acts_as_tenant gem to enforce automatic row-level tenant isolation, resolving the current website from incoming request headers or domains and scoping all database queries via a custom RequiresTenant concern in the PwbTenant namespace.**

PropertyWebBuilder is an open-source Rails-based real estate CMS that implements multi-tenancy through the `acts_as_tenant` gem. The architecture isolates data per tenant (website) at the ORM level while supporting cross-tenant operations for administrative tasks.

## Tenant Resolution via the SubdomainTenant Concern

Tenant identification happens in [`app/controllers/concerns/subdomain_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/subdomain_tenant.rb) through the `set_current_website_from_request` method. This concern is included in administrative controllers like `SiteAdminController` and executes as a `before_action` on every request.

The resolution logic follows a strict priority:

1. **Header override** – Checks for `X-Website-Slug` header to fetch a specific `Pwb::Website`
2. **Host lookup** – Calls `Pwb::Website.find_by_host(host)` to match custom domains or subdomains
3. **Fallback** – Defaults to the first website in the database if no match is found
4. **Binding** – Sets `ActsAsTenant.current_tenant` to the resolved website

```ruby

# app/controllers/concerns/subdomain_tenant.rb

def set_current_website_from_request
  slug = request.headers["X-Website-Slug"]
  if slug.present?
    Pwb::Current.website = Pwb::Website.find_by(slug: slug)
    ActsAsTenant.current_tenant = Pwb::Current.website and return if Pwb::Current.website
  end

  host = request.host.to_s.downcase
  Pwb::Current.website = Pwb::Website.find_by_host(host) || Pwb::Website.first
  ActsAsTenant.current_tenant = Pwb::Current.website
end

```

The `SiteAdminController` explicitly calls `set_tenant_from_subdomain` to ensure `ActsAsTenant.current_tenant` is always synchronized with `current_website` before any tenant-scoped queries execute.

## Global ActsAsTenant Configuration

The gem's behavior is configured in [`config/initializers/acts_as_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/initializers/acts_as_tenant.rb) with `require_tenant` set to `false`. This permissive setting allows non-tenant-scoped models to operate without a tenant context, while the `PwbTenant::` namespace enforces strict tenant presence through its own concern.

```ruby

# config/initializers/acts_as_tenant.rb

ActsAsTenant.configure do |config|
  config.require_tenant = false   # only the PwbTenant:: models require a tenant

end

```

This configuration creates a hybrid architecture where core models (`Pwb::Website`, `Pwb::User`) remain globally accessible, while content models (`PwbTenant::SaleListing`, `PwbTenant::WebsitePhoto`) are strictly isolated.

## Model-Level Tenant Enforcement with PwbTenant

All tenant-isolated models reside under the `PwbTenant` namespace and include the `RequiresTenant` concern from [`app/models/concerns/pwb_tenant/requires_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/concerns/pwb_tenant/requires_tenant.rb). This concern injects a **default_scope** that validates tenant presence before executing queries.

```ruby

# app/models/concerns/pwb_tenant/requires_tenant.rb

included do
  default_scope do
    if ActsAsTenant.current_tenant.nil? && !ActsAsTenant.unscoped?
      raise ActsAsTenant::Errors::NoTenantSet,
            "#{name} requires a tenant to be set. Use Pwb::#{name.demodulize} for cross‑tenant queries, " \
            "or set a tenant with ActsAsTenant.with_tenant(website) { … }"
    end
    all
  end
end

```

Individual models declare `acts_as_tenant :website` with the specific class name. For example, [`app/models/pwb_tenant/website_photo.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb_tenant/website_photo.rb) inherits from the global `Pwb::WebsitePhoto` but enforces tenant isolation:

```ruby

# app/models/pwb_tenant/website_photo.rb

class WebsitePhoto < Pwb::WebsitePhoto
  include RequiresTenant
  acts_as_tenant :website, class_name: 'Pwb::Website'
end

```

When a tenant is set, queries like `PwbTenant::WebsitePhoto.all` automatically generate SQL with `WHERE "pwb_website_photos"."website_id" = <current_website_id>`, eliminating the risk of cross-tenant data leaks.

## Querying and Creating Tenant-Scoped Records

Controllers inheriting from `SiteAdminController` automatically scope all database interactions to the resolved tenant. Developers never manually append `where(website_id: ...)` clauses.

**Fetching records within the current tenant:**

```ruby
class ListingsController < SiteAdminController
  def index
    # Automatically limited to the current website

    @listings = PwbTenant::SaleListing.order(created_at: :desc)
  end
end

```

**Creating new tenant-scoped records:**

```ruby
def create
  @listing = PwbTenant::SaleListing.new(listing_params)
  @listing.save!   # website_id is set automatically via acts_as_tenant

  redirect_to @listing
end

```

**Running cross-tenant queries:**

Use the base `Pwb::` namespace models to bypass tenant scoping for administrative reports or rake tasks:

```ruby

# Use the non‑tenant version to bypass scoping

Pwb::SaleListing.where('price > ?', 500_000)   # no website_id filter

```

**Temporarily switching tenants:**

Administrators can scope queries to a different tenant using `ActsAsTenant.with_tenant`:

```ruby
ActsAsTenant.with_tenant(other_website) do
  PwbTenant::Message.where(read: false)   # scoped to `other_website`

end

```

## Summary

- **Tenant resolution** occurs in [`app/controllers/concerns/subdomain_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/subdomain_tenant.rb) via headers, domains, or fallback logic
- **Global configuration** in [`config/initializers/acts_as_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/initializers/acts_as_tenant.rb) sets `require_tenant = false` to allow hybrid tenant/global models
- **Enforcement** happens through [`app/models/concerns/pwb_tenant/requires_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/concerns/pwb_tenant/requires_tenant.rb), which adds a default scope raising errors when tenants are missing
- **Isolation** is automatic for all `PwbTenant::` models via `acts_as_tenant :website`, injecting `website_id` filters into every query
- **Cross-tenant operations** use the base `Pwb::` namespace or `ActsAsTenant.with_tenant` blocks for temporary context switching

## Frequently Asked Questions

### How does PropertyWebBuilder determine which tenant to use for an incoming request?

The `SubdomainTenant` concern in [`app/controllers/concerns/subdomain_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/subdomain_tenant.rb) checks the `X-Website-Slug` header first, then falls back to matching the request host against `Pwb::Website.find_by_host`, and finally defaults to the first website in the database if no match is found.

### What happens if a PwbTenant model is queried without setting a tenant first?

The `RequiresTenant` concern raises `ActsAsTenant::Errors::NoTenantSet` because its default_scope validates that `ActsAsTenant.current_tenant` is present before executing the query, preventing accidental cross-tenant data exposure.

### Can administrators query data across all tenants in PropertyWebBuilder?

Yes. Use the global namespace models (e.g., `Pwb::SaleListing` instead of `PwbTenant::SaleListing`) to bypass tenant scoping entirely, or use `ActsAsTenant.with_tenant(other_website) { ... }` to temporarily scope operations to a specific tenant.

### Where is the acts_as_tenant gem configured to allow some models without tenants?

The initializer at [`config/initializers/acts_as_tenant.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/initializers/acts_as_tenant.rb) sets `config.require_tenant = false`, allowing models outside the `PwbTenant` namespace to operate without a tenant context while the `RequiresTenant` concern enforces tenant presence only for namespace-specific models.