> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

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

> Set up Spree as a multi-tenant SaaS ecommerce platform — install the Enterprise gem, apply license keys, configure subdomains, and provision tenants.

<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](/docs/developer/multi-vendor) instead.
</Info>

## Prerequisites

* You need to be on Spree 5.1+, we recommend using [CLI](/docs/developer/getting-started) 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 theme={"theme":"night-owl"}
spree eject
```

### Adding gems

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

   ```ruby theme={"theme":"night-owl"}
   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](https://docs.docker.com/build/building/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 backend/Dockerfile theme={"theme":"night-owl"}
   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 docker-compose.yml theme={"theme":"night-owl"}
   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 CLI (Docker) theme={"theme":"night-owl"}
     spree bundle install
     ```

     ```bash Without Spree CLI theme={"theme":"night-owl"}
     bundle install
     ```
   </CodeGroup>

4. Run generators:

   <CodeGroup>
     ```bash Spree CLI (Docker) theme={"theme":"night-owl"}
     spree generate spree_enterprise:install && spree generate spree_multi_tenant:install
     ```

     ```bash Without Spree CLI theme={"theme":"night-owl"}
     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 theme={"theme":"night-owl"}
Spree.root_domain = 'example.com'
```

or use environment variable:

```ruby theme={"theme":"night-owl"}
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 domain          | `COOKIE_TLD_LENGTH` | Cookie domain    |
| -------------------- | ------------------- | ---------------- |
| `lvh.me`             | `2`                 | `.lvh.me`        |
| `example.com`        | `2`                 | `.example.com`   |
| `shop.example.co.uk` | `3`                 | `.example.co.uk` |

```bash .env theme={"theme":"night-owl"}
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 backend/config/environments/development.rb theme={"theme":"night-owl"}
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 backend/app/models/spree/user.rb theme={"theme":"night-owl"}
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 backend/app/models/spree/admin_user.rb theme={"theme":"night-owl"}
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>
