mcpField
S
spree-multi-tenant

spree-multi-tenant

github.com/spree開発10101 回インストール4 週間前に更新
リポジトリ

spree-multi-tenant について

ライセンスが必要な Enterprise gem を使い、Spree 6 でマルチテナント SaaS を運用する開発者向けスキルです。テナント分離、リクエスト時の識別、導入設定、テナント対応コードを説明します。

主な機能

  1. 01行レベルでのテナントデータ分離
  2. 02ホスト名によるテナント解決
  3. 03ストアと顧客のテナントモデル
  4. 04テナント単位のジョブとコード
  5. 05Enterprise の導入手順

活用シーン

  1. 01独立したマーチャント向け SaaS の構築
  2. 02テナント間の顧客データ分離
  3. 03テナントストアと初期設定の作成
  4. 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、ダッシュボードのすべてに適用されます。

インストール(概要)

  1. サーバー環境(および CI、ステージング、本番のビルド環境)に KEYGEN_ACCOUNT_ID と KEYGEN_LICENSE_KEY を設定します。

  2. Docker CLI のフローを使用している場合は spree eject を実行します(gem を独自のイメージにインストールする必要があります)。

  3. 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 のみ:server/Dockerfile 内のすべての bundle install に、BuildKit シークレットとして Keygen の値を渡します(RUN --mount=type=secret,id=keygen_license_key,env=KEYGEN_LICENSE_KEY ...)。両方の compose ファイルでシークレットを宣言してください。ライセンスがイメージレイヤーに書き込まれるため、ARG/ENV は決して使用しないでください。

  5. 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.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(エンタープライズライセンス)
  • 関連スキル:spree-data-model(ストア、チャネル)、spree-marketplace、spree-deployment、spree-security

コメント