skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
spree/agent-skills107 installs

spree-multi-tenant

Use when building a SaaS / multi-tenant platform on Spree 6 with the Enterprise `spree_multi_tenant` gem — many isolated merchants on one deployment, each with its own stores, catalog, customers and orders. Covers licensing and install (`spree_enterprise:install`, `spree_multi_tenant:install`), `Spree::Tenant` and row-level `tenant_id` isolation, `SpreeMultiTenant::Base`, `with_tenant` / `without_tenant`, console tenant context, `insert_all`/`upsert_all` pitfalls, background jobs, `Spree.root_domain`, cookie domain and host authorization, global staff vs per-tenant customers. Common phrasings include "multi-tenant Spree", "SaaS on Spree", "each merchant gets a subdomain", "tenant isolation", "records leaking between tenants", "set the tenant in the console", "Blocked hosts lvh.me".

How do I install this agent skill?

npx skills add https://github.com/spree/agent-skills --skill spree-multi-tenant
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    This skill provides secure guidelines and setup instructions for the Spree Multi-Tenant enterprise extension. It follows industry best practices for secret management, specifically recommending the use of environment variables and Docker BuildKit secrets for license keys, and provides explicit developer guidance to prevent data isolation vulnerabilities.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

What does this agent skill do?

Multi-Tenant (SaaS) Spree

Enterprise only. Multi-tenancy ships in the licensed spree_enterprise + spree_multi_tenant gems (private Keygen gem source). It is not part of open-source Spree. Confirm with Spree which gem release supports your Spree 6 version before planning around it.

Multi-tenant vs multi-store vs marketplace

NeedUse
Many independent merchants, fully isolated data, one deployment (Shopify-style SaaS)Multi-tenant (this skill)
One merchant running several storefronts that share a catalog/customersMultiple Spree::Stores (+ spree_multi_store for cross-store sharing) — see spree-data-model
Many sellers listing products under one shared storefrontMarketplace — see spree-marketplace

How isolation works

  • Spree::Tenant is the top-level record. Creating a tenant creates its store and runs Spree's seeds (countries, zones, tax categories…).
  • Row-level tenancy: every tenanted table has a tenant_id column. The gem is built on acts_as_tenant.
  • The gem sets Spree.base_class to SpreeMultiTenant::Base, so every Spree model that inherits from Spree.base_class is tenanted automatically. SpreeMultiTenant::Base also makes spree_base_uniqueness_scope return [:tenant_id], so uniqueness validations (slugs, codes, SKUs) are per tenant.
  • While a tenant is current, all reads add WHERE tenant_id = ? and all writes set tenant_id automatically.
  • The current tenant is resolved per request from the host: tenant subdomain (store1.example.com) or a custom domain → store → store.tenant_id. Available as current_tenant in controllers and SpreeMultiTenant.current_tenant elsewhere.
  • One tenant can own several stores. Spree::Store.default returns the default store of the first tenant — never rely on it in tenant-aware code.
  • Staff (admin users) are global and can be invited to many tenants. Customers are per tenant — the same email can register at two tenants.
  • Isolation applies to the Admin API, Store API and dashboard alike.

Install (summary)

  1. Set KEYGEN_ACCOUNT_ID and KEYGEN_LICENSE_KEY in the server environment (and CI, staging, production build environments).

  2. spree eject if you use the Docker CLI flow (gems must be installed into your own image).

  3. Add to server/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
    
  4. Docker only: pass the Keygen values to every bundle install in server/Dockerfile as BuildKit secrets (RUN --mount=type=secret,id=keygen_license_key,env=KEYGEN_LICENSE_KEY ...) and declare the secrets in both compose files. Never use ARG/ENV — that bakes the license into image layers.

  5. spree bundle install (or bundle install), then:

    spree generate spree_enterprise:install && spree generate spree_multi_tenant:install
    # classic: bin/rails g spree_enterprise:install && bin/rails g spree_multi_tenant:install
    

    This copies + runs migrations, wraps the Spree engine mount in config/routes.rb with domain constraints (and mounts the tenant app on the app subdomain), includes SpreeMultiTenant::CustomerUserConcern in the customer model, and changes the admin user's parent to Spree::Base.

Post-install configuration

# server/config/initializers/spree.rb
Spree.root_domain = ENV.fetch('SPREE_ROOT_DOMAIN', 'lvh.me')
# server/.env
SPREE_ROOT_DOMAIN=lvh.me   # local dev — NOT localhost (browsers reject localhost as a cookie domain)
COOKIE_TLD_LENGTH=2        # labels in the root domain: example.com=2, shop.example.co.uk=3
  • COOKIE_TLD_LENGTH scopes the session cookie to the parent domain so staff stay signed in across app.<root> and tenant subdomains. Symptom when wrong: logged out between subdomains, ActionController::InvalidAuthenticityToken.

  • Host authorization: allow the root domain or requests die with "Blocked hosts":

    # server/config/environments/development.rb (and production equivalent)
    if (root_domain = ENV['SPREE_ROOT_DOMAIN']).present?
      config.hosts << ".#{root_domain}"
    end
    
  • Customer model: after the generator adds SpreeMultiTenant::CustomerUserConcern, remove Devise's :validatable from the customer model — it enforces globally-unique emails; the concern re-validates per tenant. Don't add other Devise modules. The admin user model keeps :validatable (staff emails are global).

  • config/initializers/spree_multi_tenant.rb:

    SpreeMultiTenant::Config[:app_subdomain] = 'app'   # app.example.com — signup at /tenants/new
    SpreeMultiTenant::Config[:mail_from_name] = ENV.fetch('MAIL_FROM_NAME', 'Your SaaS')
    SpreeMultiTenant::Config[:mail_from_address] = ENV.fetch('MAIL_FROM_ADDRESS', "support@#{Spree.root_domain}")
    

    mail_from_* is used for staff-facing mail (invitations, exports). Customer emails use the tenant store's name and set Reply-To to the store's email.

Writing tenant-aware code

New models

Tenanted models inherit from SpreeMultiTenant::Base (automatic if you use Spree.base_class) and need a tenant_id column. Don't declare belongs_to :tenant — the gem does.

spree generate model Spree::Brand tenant:references name:string --parent SpreeMultiTenant::Base

If you use spree generate api_resource / spree:model, add the tenant_id column and index yourself, and include tenant_id in any unique DB index ([:tenant_id, :slug]).

Forcing / escaping tenant context

SpreeMultiTenant.with_tenant(tenant) do
  # everything here is scoped to tenant
end

SpreeMultiTenant.without_tenant do
  # cross-tenant: platform billing, global reports, maintenance
end

Console / rails runner / rake tasks start with no tenant:

SpreeMultiTenant.current_tenant = Spree::Store.find_by(code: 'my_store').tenant
Spree::Current.store = Spree::Store.find_by(code: 'my_store')  # Spree 6 code also expects a current store

Provisioning a new merchant's store

Seed it inside the tenant with Spree::Seeds::StoreResources.call(store: store) (per-store seeds — channels, roles, payment methods, API keys, … — without touching other stores), then Spree::Stores::ProvisionDefaults.call(store:, country:) for the country-shaped defaults (market, warehouse, delivery zones, pickup). Optional demo data: Spree::SampleData::LoadJob.perform_later(store.id). See spree-data-model.

Background jobs

Jobs enqueued while a tenant is current run in that tenant's context. Jobs enqueued outside one (cron/recurring jobs, without_tenant blocks) must iterate tenants explicitly:

Spree::Tenant.find_each do |tenant|
  SpreeMultiTenant.with_tenant(tenant) { MyApp::NightlySyncJob.perform_later }
end

Gotchas

  • insert_all / upsert_all / raw SQL bypass tenancy — set tenant_id on every row yourself or you create orphaned/cross-tenant rows.
  • Spree::Store.default / Spree::Store.first in tenant code returns the wrong tenant's store — use current_store / Spree::Current.store.
  • Unique DB indexes without tenant_id make the second tenant's "same" slug/SKU fail at the DB level even though validations pass.
  • localhost for local dev breaks cookies; use lvh.me or localtest.me.
  • Keygen credentials missing in the Docker build step (not just runtime) → bundle install 401s.
  • Tenanted records (including webhook endpoints) are only visible inside their tenant; a subscriber or job doing cross-tenant work must use without_tenant deliberately.
  • Third-party extensions that write with insert_all or define their own base class may need a tenant_id migration and decorator — test each extension with two tenants.

Where to read further

  • node_modules/@spree/docs/dist/developer/multi-tenant/quickstart.md
  • node_modules/@spree/docs/dist/developer/multi-tenant/core-concepts.md
  • node_modules/@spree/docs/dist/developer/multi-tenant/configuration.md
  • https://spreecommerce.org/pricing (Enterprise license)
  • Related skills: spree-data-model (stores, channels), spree-marketplace, spree-deployment, spree-security

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/spree/agent-skills/spree-multi-tenant">View spree-multi-tenant on skillZs</a>