Back to Spree

Create a multi-tenant / SaaS platform on top of Spree

docs/developer/multi-tenant/quickstart.mdx

5.6.09.3 KB
Original Source
<Info> Multi-tenancy requires [Spree Enterprise Edition license](https://spreecommerce.org/pricing). </Info>

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.

<Info> If you need individual seller/supplier/vendor accounts but shared product listings under one site you should use [multi vendor recipe](/developer/multi-vendor) instead. </Info>

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
<Info> You will need to add these environment variables to your CI/CD pipeline and staging/production environments. </Info>

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:

bash
spree eject

Adding gems

  1. Add the following code to your backend/Gemfile:

    ruby
    source "https://license:#{ENV['KEYGEN_LICENSE_KEY']}@rubygems.pkg.keygen.sh/#{ENV['KEYGEN_ACCOUNT_ID']}" do
      gem 'spree_enterprise'
      gem 'spree_multi_tenant'
    end
    
  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).

    <Info> 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. </Info>

    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:

    dockerfile
    COPY .ruby-version Gemfile Gemfile.lock ./
    RUN bundle install # [!code --]
    RUN --mount=type=secret,id=keygen_license_key,env=KEYGEN_LICENSE_KEY,required=false \ # [!code ++]
      --mount=type=secret,id=keygen_account_id,env=KEYGEN_ACCOUNT_ID,required=false \ # [!code ++]
      bundle install # [!code ++]
    

    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:

    yaml
    x-app: &app
      build:
        context: ./backend
        dockerfile: Dockerfile
        target: dev
        secrets: # [!code ++]
          - keygen_license_key # [!code ++]
          - keygen_account_id # [!code ++]
    
    # ...at the bottom of the file, declare where the secrets come from:
    secrets: # [!code ++]
      keygen_license_key: # [!code ++]
        environment: KEYGEN_LICENSE_KEY # [!code ++]
      keygen_account_id: # [!code ++]
        environment: KEYGEN_ACCOUNT_ID # [!code ++]
    
    <Info> 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). </Info>
  3. Install gems:

    <CodeGroup>
    bash
    spree bundle install
    
    bash
    bundle install
    
    </CodeGroup>
  4. Run generators:

    <CodeGroup>
    bash
    spree generate spree_enterprise:install && spree generate spree_multi_tenant:install
    
    bash
    bin/rails g spree_enterprise:install && bin/rails g spree_multi_tenant:install
    
    </CodeGroup> <Info> 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](#post-install-configuration) below. </Info>

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.

ruby
Spree.root_domain = 'example.com'

or use environment variable:

ruby
Spree.root_domain = ENV.fetch('SPREE_ROOT_DOMAIN', 'lvh.me')

You need to use lvh.me for local development, so cross-subdomain cookies will work, localhost will not work.

<Warning> `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. </Warning>

Sharing the session cookie across subdomains

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:

Root domainCOOKIE_TLD_LENGTHCookie domain
lvh.me2.lvh.me
example.com2.example.com
shop.example.co.uk3.example.co.uk
bash
SPREE_ROOT_DOMAIN=lvh.me
COOKIE_TLD_LENGTH=2

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:

ruby
if (root_domain = ENV['SPREE_ROOT_DOMAIN']).present?
  config.hosts << ".#{root_domain}"
end

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.

ruby
class Spree::User < Spree.base_class
  include Spree::UserAddress
  include Spree::UserMethods
  include Spree::UserPaymentSource
  include SpreeMultiTenant::CustomerUserConcern # added by the generator

  devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable # [!code --]
  devise :database_authenticatable, :registerable, :recoverable, :rememberable # [!code ++]
end
<Warning> 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. </Warning>

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:

ruby
class Spree::AdminUser < Spree.base_class # [!code --]
class Spree::AdminUser < Spree::Base # [!code ++]
  include Spree::AdminUserMethods

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
<Info> 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. </Info>