# PropertyWebBuilder Website Provisioning Flow and Tenant Initialization Guide

> Understand the PropertyWebBuilder website provisioning flow and tenant initialization. Learn how Pwb::ProvisioningService manages websites from signup to live, including database sharding and seed data.

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

---

**The PropertyWebBuilder platform provisions new tenants through a state-machine-driven workflow in `Pwb::ProvisioningService`, transitioning websites from signup through email verification to a live state while handling database sharding and seed data initialization.**

The **PropertyWebBuilder** repository (`etewiah/property_web_builder`) implements a multi-tenant SaaS architecture where each tenant is isolated as a distinct `Pwb::Website` record. Understanding the website provisioning flow and tenant initialization is essential for developers extending the platform, customizing onboarding experiences, or debugging tenant creation failures.

## Understanding the Provisioning Architecture

The provisioning system centers on the **`Pwb::ProvisioningService`** class located in [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb). This service orchestrates the complete tenant lifecycle—from initial email signup through sub-domain reservation, database sharding, and content seeding—while maintaining strict state consistency through an AASM (Acts As State Machine) implementation.

Multi-tenancy is achieved by combining sub-domain isolation with database sharding. Each `Website` record stores a `database_shard` identifier that routes queries to the correct physical database partition, ensuring tenant data remains segregated at the infrastructure level.

## The Five-Step Website Provisioning Flow

The provisioning service executes a sequential workflow defined in [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb). Each step wraps database operations in transactions and reports progress through optional callback blocks.

### Step 1: Signup and Sub-domain Reservation (`start_signup`)

The flow begins when `start_signup(email:)` creates a lead `User` record without a password and reserves a temporary sub-domain for 10 minutes via `Subdomain.reserve_for_email`. This method also triggers platform notifications through `notify_platform(:user_signup, ...)`.

```ruby
service = Pwb::ProvisioningService.new
signup = service.start_signup(email: 'alice@example.com')

```

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 20-70.

### Step 2: Email Verification (`verify_email`)

Once the user receives their token, `verify_email(user:, token:)` validates the email address by calling `user.verify_email!` and notifies the platform via `:email_verified`. This step transitions the user entity to a verified state but does not yet modify the website state machine.

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 87-99.

### Step 3: Site Configuration (`configure_site`)

The `configure_site(user:, subdomain_name:, site_type:)` method validates the custom sub-domain through `SubdomainGenerator.validate_custom_name` and confirms the `site_type` against `Website::SITE_TYPES`. It then instantiates a new `Website` with `provisioning_state: 'pending'`, creates an ownership `UserMembership` with role `'owner'`, and assigns the owner via `website.assign_owner!`—triggering the transition to `owner_assigned`.

```ruby
config = service.configure_site(
  user: signup[:user],
  subdomain_name: 'alice-realty',
  site_type: 'residential'
)

```

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 103-166.

### Step 4: Website Provisioning (`provision_website`)

The core tenant initialization occurs in `provision_website(website:, skip_properties: false, &progress_block)`. This method verifies the starting state (`pending` or `owner_assigned`), then sequentially creates:

- Agency records
- Navigation links (minimum 3)
- Field keys (minimum 5)
- Pages
- Optional property seed data

Each milestone updates the `provisioning_state` via `report_progress`, progressing through `agency_created`, `links_created`, `field_keys_created`, `pages_created`, and `properties_seeded`. Final verification via `website.provisioning_complete?` triggers `mark_ready!`, followed by `enter_locked_state!` and `send_verification_email`.

```ruby
result = service.provision_website(website: config[:website]) do |progress|
  puts "Progress: #{progress[:percentage]}% – #{progress[:state]}"
end

```

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 174-279.

### Step 5: Retry Logic on Failure (`retry_provisioning`)

If any step raises an exception, the `fail_with_details` method captures the error in `@errors` and the `provisioning_error` column, transitioning the site to `failed`. The `retry_provisioning(website:)` method resets the state to `owner_assigned` via `website.retry_provisioning!` and re-invokes the provisioning sequence.

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 296-306 and 488-492.

## AASM State Machine Reference

The `Website` model includes the `Pwb::WebsiteProvisionable` concern defining the following states in the `provisioning_state` column:

- **`pending`** – Record created but owner not assigned.
- **`owner_assigned`** – Owner membership confirmed; ready for agency creation.
- **`agency_created`** – Agency record present.
- **`links_created`** – Minimum navigation links exist.
- **`field_keys_created`** – Minimum field keys exist.
- **`pages_created`** – At least one page exists.
- **`properties_seeded`** – Optional seed data added.
- **`ready`** – All mandatory items present.
- **`locked_pending_email_verification`** – Site ready but locked until owner verifies email.
- **`live`** – Final active state after `website.activate!`.
- **`failed`** – Error captured; requires retry.

## Tenant Initialization Mechanics

### Database Sharding Strategy

Each `Website` instance specifies its shard via the `database_shard` method (defined in [`app/models/pwb/website.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/website.rb) lines 21-24). The provisioning service automatically routes all queries to the correct shard using Rails 6+ multi-database support, defaulting to `"default"` when no explicit shard is configured.

### Ownership and UserMembership

Tenant ownership is established through `UserMembership` records linking a `User` to a `Website`. During `configure_site`, the service creates:

```ruby
UserMembership.create!(
  user: user,
  website: website,
  role: 'owner',
  active: true
)

```

This membership is mandatory for the `owner_assigned` AASM transition.

Source: [`app/models/pwb/user_membership.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/user_membership.rb) and [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 146-152.

### Seed Packs and Customization

The provisioning flow supports theme-specific initialization via **seed packs**. The service checks for a `seed_pack_name` (defaulting to `'base'`) and loads data through `try_seed_pack_step`, allowing market-specific content injection without code changes.

```ruby
pack_name = website.seed_pack_name || 'base'
seed_pack = Pwb::SeedPack.find(pack_name)

```

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 74-89.

### Error Handling and Transactions

Each provisioning step executes within database transactions where appropriate. The `fail_with_details` method standardizes error reporting:

```ruby
def fail_with_details(website, error_message)
  @errors << "Provisioning failed: #{error_message}"
  website.fail_provisioning!(error_message) if website.may_fail_provisioning?
end

```

Errors persist in both the service's `@errors` array and the `website.provisioning_error` column.

Source: [`app/services/pwb/provisioning_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/provisioning_service.rb) lines 488-492.

## Complete Implementation Example

The following Ruby code demonstrates the full website provisioning flow and tenant initialization sequence:

```ruby

# Initialize the service

service = Pwb::ProvisioningService.new

# 1️⃣ Start signup flow

signup = service.start_signup(email: 'alice@example.com')
raise signup[:errors].join(', ') unless signup[:success]

# 2️⃣ Verify email (typically via controller token param)

verification = service.verify_email(
  user: signup[:user], 
  token: params[:token]
)
raise verification[:errors].join(', ') unless verification[:success]

# 3️⃣ Configure site with custom sub-domain

config = service.configure_site(
  user: signup[:user],
  subdomain_name: 'alice-realty',
  site_type: 'residential'
)
raise config[:errors].join(', ') unless config[:success]

# 4️⃣ Provision with progress tracking

result = service.provision_website(website: config[:website]) do |progress|
  Rails.logger.info "Provisioning: #{progress[:percentage]}% – #{progress[:state]}"
end
raise result[:errors].join(', ') unless result[:success]

# Site now in `locked_pending_email_verification` state

# Automatically transitions to `live` after owner activates via email

```

## Summary

- The **PropertyWebBuilder** platform uses `Pwb::ProvisioningService` to orchestrate multi-tenant onboarding through a deterministic state machine.
- **Five distinct steps**—signup, verification, configuration, provisioning, and optional retry—ensure atomic tenant creation with full error recovery.
- **Database sharding** via `website.database_shard` provides physical isolation, while `UserMembership` records enforce role-based ownership.
- **AASM states** track progress from `pending` through `live`, with `locked_pending_email_verification` ensuring security before public access.
- **Seed packs** enable customizable content injection without modifying core provisioning logic.

## Frequently Asked Questions

### How does PropertyWebBuilder handle database isolation between tenants?

The platform implements horizontal sharding where each `Website` record stores a `database_shard` identifier. All queries within the provisioning flow automatically route to the correct shard via Rails 6+ multi-database support, defaulting to the `"default"` shard if none is specified. This ensures complete data isolation at the database connection level.

### What happens if the provisioning process fails midway?

If any step raises an exception or fails a guard condition, the `fail_with_details` method captures the error message in both the service's `@errors` array and the `website.provisioning_error` column, transitioning the state to `failed`. Administrators can then invoke `retry_provisioning(website:)` to reset the state to `owner_assigned` and re-run the full sequence without manual database cleanup.

### Can I customize the initial content seeded to new tenants?

Yes. The provisioning service supports **seed packs** defined by the `seed_pack_name` attribute on the `Website` model. During initialization, the service attempts to load data from `Pwb::SeedPack.find(pack_name)`, allowing you to inject custom themes, property types, or regional content by creating new seed pack configurations without modifying the core `ProvisioningService` code.

### Why is the site locked after provisioning completes?

After successfully reaching the `ready` state, the site immediately transitions to `locked_pending_email_verification` and sends a verification email. This security measure prevents public access to the tenant site until the owner confirms their email address. Once verified, the `activate!` method transitions the site to the final `live` state, making it publicly accessible.