PropertyWebBuilder Website Provisioning Flow and Tenant Initialization Guide
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. 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. 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, ...).
service = Pwb::ProvisioningService.new
signup = service.start_signup(email: 'alice@example.com')
Source: 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 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.
config = service.configure_site(
user: signup[:user],
subdomain_name: 'alice-realty',
site_type: 'residential'
)
Source: 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.
result = service.provision_website(website: config[:website]) do |progress|
puts "Progress: #{progress[:percentage]}% – #{progress[:state]}"
end
Source: 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 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 afterwebsite.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 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:
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 and 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.
pack_name = website.seed_pack_name || 'base'
seed_pack = Pwb::SeedPack.find(pack_name)
Source: 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:
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 lines 488-492.
Complete Implementation Example
The following Ruby code demonstrates the full website provisioning flow and tenant initialization sequence:
# 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::ProvisioningServiceto 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_shardprovides physical isolation, whileUserMembershiprecords enforce role-based ownership. - AASM states track progress from
pendingthroughlive, withlocked_pending_email_verificationensuring 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.
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 →