Skip to main content
Multi-tenancy requires Spree Enterprise Edition license.
Spree can be configured to run a multi-tenant / SaaS platform. This guide will walk you through the steps to set up your Spree application to support multiple tenants (stores). Each tenant (store) can have its own isolated data and configuration, including:
  • customer accounts
  • staff accounts (admin users), staff can manage multiple tenants (it’s the standard invitation flow)
  • products, categories, and other catalog data
  • orders and order history
  • shipping and payment methods
  • tax rates and zones
  • store settings (name, logo, etc.)
  • etc.
All data is fully isolated besides the staff users, which can manage multiple tenants. This allows you to create a SaaS platform where each tenant can have its own store with its own branding and configuration. Isolation works across admin dashboard and API.
If you need individual seller/supplier/vendor accounts but shared product listings under one site you should use multi vendor recipe instead.

Prerequisites

  • You need to be on Spree 5.1+, we recommend using CLI to setup your Spree application.
  • You need to set 2 environment variables in your backend directory:
    • KEYGEN_ACCOUNT_ID
    • KEYGEN_LICENSE_KEY
  • Both PostgreSQL and MySQL are supported
You will need to add these environment variables to your CI/CD pipeline and staging/production environments.

Installation

Eject the Spree backend

If you’ve created your project with create-spree-app you will need to eject the backend to be able to install the Enterprise and Multi-Tenant gems. You can do this by running:

Adding gems

  1. Add the following code to your backend/Gemfile:
  2. Configure the Docker image build to authenticate against the Keygen source. If you use the Spree CLI (Docker), spree bundle install resolves the licensed gems inside the image at build time, so the Keygen credentials must be available during bundle install in the Dockerfile — not just at runtime. Pass them as BuildKit secrets so they are mounted as environment variables for the bundle install step only, and never baked into the image layers or history (unlike ARG/ENV/COPY).
    Skip this step if you install without the Spree CLI (plain bundle install on the host). In that case the Gemfile reads KEYGEN_LICENSE_KEY / KEYGEN_ACCOUNT_ID directly from your shell environment.
    Wrap every bundle install in your backend/Dockerfile with the secret mounts. There are usually two — one in the build stage and one in the dev stage:
    backend/Dockerfile
    Then wire the secrets into both compose files (docker-compose.yml and docker-compose.dev.yml — the latter is what spree eject copies over) so the CLI supplies them to the build:
    docker-compose.yml
    Compose reads environment:-sourced secrets from the shell environment that runs the build, not from .env. Make sure KEYGEN_LICENSE_KEY and KEYGEN_ACCOUNT_ID are exported in the shell (and in your CI/CD and production build environments).
  3. Install gems:
  4. Run generators:
    This will copy and run migrations for spree_enterprise and spree_multi_tenant gems. The spree_multi_tenant:install generator also edits config/routes.rb (to add the tenant domain routing constraints), your customer user model, and your admin user model — see Post-install configuration below.

Post-install configuration

Setting root domain

Usually multi-tenant applications are configured to use subdomains for each tenant. For example, if your root domain is example.com, you can have tenants like tenant1.example.com, tenant2.example.com, etc. To make it work you need to set the Spree.root_domain in your config/initializers/spree.rb file, eg.
or use environment variable:
You need to use lvh.me for local development, so cross-subdomain cookies will work, localhost will not work.
localhost cannot be used as a cookie domain by browsers, so the session cookie set on store1.localhost is rejected and never sent back — you get logged out when moving between subdomains, and admin sign-in fails with ActionController::InvalidAuthenticityToken (“Can’t verify CSRF token authenticity”). Use lvh.me (or localtest.me); both resolve to 127.0.0.1 and are valid cookie domains.
For staff to stay signed in while moving between the app domain (app.example.com) and tenant subdomains (store1.example.com), the session cookie must be scoped to the parent domain. Set COOKIE_TLD_LENGTH to the number of labels in your root domain:
.env

Allowing tenant subdomains through host authorization

Rails’ host authorization blocks unknown hosts. .localhost and .test are permitted in development by default, but .lvh.me (and your production domain) are not — requests to app.lvh.me are rejected with a “Blocked hosts” error until you allow them. Add your root domain to config/environments/development.rb:
backend/config/environments/development.rb

Customer User Class adjustment

We need to make slight adjustments to the user class so it can work in a multi-tenant environment. The spree_multi_tenant:install generator adds include SpreeMultiTenant::CustomerUserConcern to your customer user model automatically. You then need to remove the :validatable module manually. :validatable validates the user’s email uniqueness globally; the concern re-validates it in the scope of the tenant instead, so the same email can exist across different tenants.
backend/app/models/spree/user.rb
Only remove :validatable. Do not add other Devise modules (such as :confirmable) here unless your app is already set up for them — :confirmable, for example, requires additional columns and a confirmation flow, and adding it will break sign-in.

Admin User Class adjustment

The generator also changes your admin user model’s base class from Spree.base_class to Spree::Base. If your admin_user.rb exists, this is applied automatically; otherwise make the change yourself:
backend/app/models/spree/admin_user.rb
Unlike the customer user model, the admin user model keeps :validatable — staff accounts are global and can manage multiple tenants, so their emails remain globally unique.