docs/developer/multi-tenant/quickstart.mdx
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:
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>backend directory:
KEYGEN_ACCOUNT_IDKEYGEN_LICENSE_KEYIf 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:
spree eject
Add the following code to your backend/Gemfile:
source "https://license:#{ENV['KEYGEN_LICENSE_KEY']}@rubygems.pkg.keygen.sh/#{ENV['KEYGEN_ACCOUNT_ID']}" do
gem 'spree_enterprise'
gem 'spree_multi_tenant'
end
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).
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:
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:
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 ++]
Install gems:
<CodeGroup>spree bundle install
bundle install
Run generators:
<CodeGroup>spree generate spree_enterprise:install && spree generate spree_multi_tenant:install
bin/rails g spree_enterprise:install && bin/rails g spree_multi_tenant:install
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.
Spree.root_domain = 'example.com'
or use environment variable:
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.
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 |
SPREE_ROOT_DOMAIN=lvh.me
COOKIE_TLD_LENGTH=2
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:
if (root_domain = ENV['SPREE_ROOT_DOMAIN']).present?
config.hosts << ".#{root_domain}"
end
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.
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
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:
class Spree::AdminUser < Spree.base_class # [!code --]
class Spree::AdminUser < Spree::Base # [!code ++]
include Spree::AdminUserMethods
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
end