spree/agent-skills

spree-multi-tenant

Use when building a SaaS / multi-tenant platform on Spree 6 with the Enterprise spreemultitenant gem — many isolated merchants on one deployment, each with its own stores, catalog, customers and orders.

Quelltext ansehen

Verwendung

  1. Installationsbefehl kopieren und das Quellpaket vor der Installation in der eigenen Agentenumgebung prüfen.
  2. Benötigte Werkzeuge, Zugangsdaten und Hosts im Original prüfen.
  3. Eingabe und gewünschtes Ergebnis beschreiben und die Ausgabe mit den Quellen abgleichen.
npx skills add https://github.com/spree/agent-skills --skill spree-multi-tenant

Vor dem Start

  • Die Installation umfasst weder Modellabo noch API-Kontingent oder Dateizugriffsrechte.
  • Sterne und Installationen zeigen Aktivität, keine unabhängige Qualitäts- oder Sicherheitsprüfung.
Originales Skill-Dokument

Aus dem Quell-Repository gerendert; Überschriften, Beispiele, Code, Tabellen, Links und Bilder bleiben erhalten.

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:
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
  1. 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.
  2. spree bundle install (or bundle install), then:
bash
   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

ruby
# server/config/initializers/spree.rb
Spree.root_domain = ENV.fetch('SPREE_ROOT_DOMAIN', 'lvh.me')
bash
# 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":
ruby
  # 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:
ruby
  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.

bash
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

ruby
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:

ruby
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:

ruby
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
aus demselben Repository

Weitere Skills

Alle Skills
spree
Community

spree-deployment

Use when deploying a Spree 6 application to production or scaling it — Docker image builds, Render/AWS/Railway/Kubernetes/VPS, required environment variables, Solid Queue (jobs in Puma vs a dedicated bin/jobs worker), Solid Cache, optional Sidekiq/Redis, the Mission Control /jobs dashboard, serving the React dashboard at /dashboard or from a static host, migrations on boot and spree:upgrade, S3/R2 storage, CDNHOST, Meilisearch, PostgreSQL/MySQL/SQLite, and observability (spreeopentelemetry, Sent

Installationen
1
GitHub Stars
10
Aktualisiert
30. Sept.
spree
Community

spree-order-totals

Use when the user is working with money on a Spree 6 cart or order — how total is built, the typed money rows (Spree::TaxLine, Spree::Discount, Spree::Fee), totals columns (itemtotal, discounttotal, deliverytotal, feetotal, includedtaxtotal vs additionaltaxtotal, adjustmenttotal, amountdue), Spree::Carts::RecalculateTotals, custom adjusters (Spree.adjusters), fees and customs duties, manual discounts and fees on placed orders, or rendering an order summary. Common phrasings include "order total

Installationen
1
GitHub Stars
10
Aktualisiert
30. Sept.
spree
Community

spree-performance

Use when investigating or improving Spree 6 performance — slow cart writes, slow checkout, slow Store API product listings, N+1 queries in custom endpoints or serializers, search latency, Meilisearch indexing floods, image processing backlogs, Solid Queue/Sidekiq queue tuning, HTTP/CDN caching of the Store API, dashboard data-fetching, or tracing with OpenTelemetry. Common phrasings include "slow cart", "add to cart is slow", "tax provider called too often", "N+1 in Spree", "slow product listing

Installationen
1
GitHub Stars
10
Aktualisiert
30. Sept.
spree
Community

spree-pricing

Use when the user is working with Spree 6 pricing — variant prices per currency (pricein, amountin, setprice), compare-at/sale prices, multi-currency, price lists and price rules (market, channel, volume), catalog-owned price lists for B2B/wholesale, percentage price adjustments, quantity breaks / volume pricing, scheduled sales, the pricing context and resolution order, external pricing providers, bulk price updates, and EU Omnibus price history (priorprice). Common phrasings include "set price

Installationen
1
GitHub Stars
10
Aktualisiert
30. Sept.