spree-multi-tenant について
ライセンスが必要な Enterprise gem を使い、Spree 6 でマルチテナント SaaS を運用する開発者向けスキルです。テナント分離、リクエスト時の識別、導入設定、テナント対応コードを説明します。
主な機能
- 01行レベルでのテナントデータ分離
- 02ホスト名によるテナント解決
- 03ストアと顧客のテナントモデル
- 04テナント単位のジョブとコード
- 05Enterprise の導入手順
活用シーン
- 01独立したマーチャント向け SaaS の構築
- 02テナント間の顧客データ分離
- 03テナントストアと初期設定の作成
- 04各テナント向け定期ジョブの実行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-multi-tenantインストール後、Claude Code で/spree-multi-tenantを入力して読み込まれたか確認します。
詳細
マルチテナント(SaaS)Spree
エンタープライズ版のみです。 マルチテナント機能は、ライセンスが必要な
spree_enterpriseとspree_multi_tenantの gem(非公開の Keygen gem ソース)に含まれています。オープンソースの Spree には含まれていません。導入を計画する前に、お使いの Spree 6 のバージョンに対応する gem のリリースを Spree に確認してください。
マルチテナントとマルチストア、マーケットプレイスの違い
| ニーズ | 使用するもの |
|---|---|
| 独立した多数のマーチャントが、データを完全に分離して、1つのデプロイメントを利用する(Shopify 型の SaaS) | マルチテナント(このスキル) |
| 1つのマーチャントが、カタログや顧客を共有する複数のストアフロントを運営する | 複数の Spree::Store(ストア間の共有には spree_multi_store)— spree-data-model を参照 |
| 多数の販売者が1つの共有ストアフロントに商品を出品する | マーケットプレイス — spree-marketplace を参照 |
分離の仕組み
Spree::Tenantは最上位のレコードです。テナントを作成すると、そのストアが作成され、Spree のシードデータ(国、ゾーン、税区分など)が実行されます。- 行レベルのテナント分離:テナント対象のテーブルにはすべて
tenant_idカラムがあります。この gem はacts_as_tenantを基盤としています。 - この gem は
Spree.base_classをSpreeMultiTenant::Baseに設定するため、Spree.base_classを継承するすべての Spree モデルは自動的にテナント対象になります。また、SpreeMultiTenant::Baseによりspree_base_uniqueness_scopeは[:tenant_id]を返すため、一意性の検証(スラッグ、コード、SKU)はテナントごとに行われます。 - テナントが現在のテナントとして設定されている間は、すべての読み取りに
WHERE tenant_id = ?が追加され、すべての書き込みでtenant_idが自動的に設定されます。 - 現在のテナントは、ホストからリクエストごとに解決されます。テナントのサブドメイン(
store1.example.com)またはカスタムドメインから、ストアを経由してstore.tenant_idが取得されます。コントローラーではcurrent_tenant、それ以外ではSpreeMultiTenant.current_tenantとして利用できます。 - 1つのテナントが複数のストアを所有できます。
Spree::Store.defaultは最初のテナントのデフォルトストアを返すため、テナント対応コードでは決してこれに依存しないでください。 - スタッフ(管理者ユーザー)はグローバルで、複数のテナントに招待できます。顧客はテナントごとです。同じメールアドレスで2つのテナントに登録できます。
- 分離は Admin API、Store API、ダッシュボードのすべてに適用されます。
インストール(概要)
-
サーバー環境(および CI、ステージング、本番のビルド環境)に
KEYGEN_ACCOUNT_IDとKEYGEN_LICENSE_KEYを設定します。 -
Docker CLI のフローを使用している場合は
spree ejectを実行します(gem を独自のイメージにインストールする必要があります)。 -
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 -
Docker のみ:
server/Dockerfile内のすべてのbundle installに、BuildKit シークレットとして Keygen の値を渡します(RUN --mount=type=secret,id=keygen_license_key,env=KEYGEN_LICENSE_KEY ...)。両方の compose ファイルでシークレットを宣言してください。ライセンスがイメージレイヤーに書き込まれるため、ARG/ENVは決して使用しないでください。 -
spree bundle install(またはbundle install)を実行し、続けて次を実行します。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これにより、マイグレーションがコピーされて実行され、
config/routes.rbの Spree エンジンのマウントがドメイン制約で囲まれ(アプリのサブドメインにはテナントアプリがマウントされます)、顧客モデルにSpreeMultiTenant::CustomerUserConcernが組み込まれ、管理者ユーザーの親クラスがSpree::Baseに変更されます。
インストール後の設定
# 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はセッション Cookie のスコープを親ドメインに設定し、スタッフがapp.<root>とテナントのサブドメイン間を移動してもログイン状態を維持できるようにします。値が誤っている場合の症状:サブドメイン間の移動でログアウトされ、ActionController::InvalidAuthenticityTokenが発生します。 -
ホストの許可設定:ルートドメインを許可しないと、リクエストが「Blocked hosts」で拒否されます。
# server/config/environments/development.rb (and production equivalent) if (root_domain = ENV['SPREE_ROOT_DOMAIN']).present? config.hosts << ".#{root_domain}" end -
顧客モデル:ジェネレーターが
SpreeMultiTenant::CustomerUserConcernを追加した後、顧客モデルから Devise の:validatableを削除してください。これによりメールアドレスがグローバルに一意であることが強制されますが、concern がテナントごとに再検証します。他の Devise モジュールは追加しないでください。管理者ユーザーモデルでは:validatableを維持してください(スタッフのメールアドレスはグローバルです)。 -
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_*はスタッフ向けメール(招待、エクスポート)に使用されます。顧客向けメールにはテナントのストア名が使用され、Reply-Toにはストアのメールアドレスが設定されます。
テナント対応コードの記述
新しいモデル
テナント対応モデルは SpreeMultiTenant::Base を継承します(Spree.base_class を使用する場合は自動的に設定されます)。また、tenant_id カラムが必要です。belongs_to :tenant は宣言しないでください。gem が処理します。
spree generate model Spree::Brand tenant:references name:string --parent SpreeMultiTenant::Base
spree generate api_resource / spree:model を使用する場合は、tenant_id カラムとインデックスを自分で追加し、一意な DB インデックスには tenant_id を含めてください([:tenant_id, :slug])。
テナントコンテキストの強制/解除
SpreeMultiTenant.with_tenant(tenant) do
# everything here is scoped to tenant
end
SpreeMultiTenant.without_tenant do
# cross-tenant: platform billing, global reports, maintenance
end
コンソール/rails runner/rake タスクは、テナントなしで開始します。
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
新しいマーチャントのストアのプロビジョニング
Spree::Seeds::StoreResources.call(store: store) を使ってテナント内でシードデータを登録します(ストア単位のシードデータ — チャネル、ロール、決済方法、API キーなど。他のストアには影響しません)。次に、国に応じたデフォルト設定(マーケット、倉庫、配送ゾーン、店舗受け取り)を Spree::Stores::ProvisionDefaults.call(store:, country:) で設定します。デモデータは任意で、Spree::SampleData::LoadJob.perform_later(store.id) を使用します。spree-data-model を参照してください。
バックグラウンドジョブ
現在のテナントがある状態でエンキューされたジョブは、そのテナントのコンテキストで実行されます。テナントの外でエンキューされたジョブ(cron/定期実行ジョブ、without_tenant ブロック)は、テナントを明示的に反復処理する必要があります。
Spree::Tenant.find_each do |tenant|
SpreeMultiTenant.with_tenant(tenant) { MyApp::NightlySyncJob.perform_later }
end
注意点
insert_all/upsert_all/ 生 SQL はテナント分離を回避します — 孤立したレコードやテナントをまたぐレコードが作成されないよう、すべての行にtenant_idを自分で設定してください。- テナントのコード内で
Spree::Store.default/Spree::Store.firstを呼び出すと、別のテナントのストアが返されます —current_store/Spree::Current.storeを使用してください。 tenant_idのない一意な DB インデックスがあると、バリデーションを通過しても、2 番目のテナントで同じ slug/SKU を作成した際に DB レベルで失敗します。- ローカル開発で
localhostを使用すると Cookie が機能しません。lvh.meまたはlocaltest.meを使用してください。 - Docker のビルドステップ(実行時だけでなく)に Keygen の認証情報がないと、
bundle installが 401 エラーになります。 - Webhook エンドポイントを含むテナント対応レコードは、そのテナント内でのみ参照できます。テナントをまたぐ処理を行うサブスクライバーやジョブでは、意図的に
without_tenantを使用する必要があります。 insert_allで書き込む、または独自のベースクラスを定義するサードパーティ拡張機能では、tenant_idのマイグレーションとデコレーターが必要になる場合があります。各拡張機能を 2 つのテナントでテストしてください。
詳細情報
node_modules/@spree/docs/dist/developer/multi-tenant/quickstart.mdnode_modules/@spree/docs/dist/developer/multi-tenant/core-concepts.mdnode_modules/@spree/docs/dist/developer/multi-tenant/configuration.md- https://spreecommerce.org/pricing(エンタープライズライセンス)
- 関連スキル:
spree-data-model(ストア、チャネル)、spree-marketplace、spree-deployment、spree-security