Back to Spree

Payment Gateways in the Monorepo

docs/plans/6.0-payment-gateways-monorepo.md

5.6.112.1 KB
Original Source

Payment Gateways in the Monorepo

Status: Design Finalized Target: Spree 6.0 Depends on: Payment sessions infrastructure (shipped in core), 6.0-multi-vendor-marketplace.md (Phase 3b consumes the Stripe gem) Author: Damian Legawiec Last updated: 2026-07-20

Summary

Move the payment-session-compatible gateway code from the standalone spree_stripe and spree_adyen repos into the monorepo as two separate, optional, lockstep-versioned gems: spree/stripe (gem spree_stripe) and spree/adyen (gem spree_adyen). Only the v3 payment-session path comes over — the gateway class, its session/setup-session models and services, payment sources, and webhook parsing. Everything serving the classic Rails storefront, the legacy admin, the legacy webhook engines, and the Storefront API v2 stays behind in the old repos, which become 5.x maintenance branches.

Why: the gateways are the only real implementations of core's payment-session contract (Spree::PaymentMethod#create_payment_session / #parse_webhook_event / #complete_payment_session, Spree::PaymentSession STI), and that contract is still evolving through 6.0. In-repo gateways give core CI real coverage (today the only in-repo implementation is Spree::PaymentSessions::Bogus), remove the cross-repo version dance on every contract change, and put the code where contributors and agents can find it. The 6.0 marketplace plan independently forces Stripe into the monorepo: Spree::PayoutProvider::StripeConnect ships OSS in-repo and needs the stripe SDK, a configured client, and webhook plumbing — keeping the gateway external would split Stripe integration across two repos.

This amends the 2026-07-15 decision's "likely into spree/core" placement: dedicated gems, not core.

Competitor context

  • Vendure bundled Stripe/Braintree/Mollie/PayPal in one in-monorepo @vendure/payments-plugin (SDKs as optional peerDependencies), then removed it in v3.6.0 to a separate community-plugins repo with one package per gateway — for community ownership and decoupled releases, after the handler contract had been stable for years. Their situation doesn't transfer: Spree's gateways are first-party (Vendo-maintained) and our contract is still moving.
  • Medusa v2 keeps exactly one first-party payment provider in the monorepo (packages/modules/providers/payment-stripe@medusajs/payment-stripe, hard stripe dependency, framework peer pinned to the exact matching version), down from five in v1; the rest are community plugins. This — the strategic gateway in-repo as the contract's reference implementation — is the model we're following, extended to Adyen.
  • Solidus (separate gem per gateway, own repos) and Saleor (external payment apps) never bundled.
  • The one-fat-package model (spree_payments) is ruled out: RubyGems has no optional dependencies, so a single gem must either hard-depend on every gateway SDK or declare none and fail at runtime. Per-gateway gems with their own SDK dependency are the Ruby translation of Vendure's optional peer deps.

Key Decisions (do not deviate without discussion)

  1. Two gems, not core, not one bundle. spree/stripe and spree/adyen directories, publishing spree_stripe and spree_adyen. Core keeps only the provider-agnostic contract (Spree::PaymentMethod session methods, Spree::PaymentSession/PaymentSetupSession, Spree::GatewayCustomer, Spree::Payments::HandleWebhook, the v3 endpoints). No spree_payments umbrella gem — nothing generic is left for it to own.
  2. Existing gem names and namespaces are kept. SpreeStripe::Gateway and SpreeAdyen::Gateway stay as-is because spree_payment_methods.type and spree_payment_sessions.type store STI class names — existing installations upgrade with no data migration and the same Gemfile line. Core's Spree::Gateway.api_type already maps these namespaces.
  3. Lockstep versioning with an exact pin. s.version = Spree.version; s.add_dependency 'spree', s.version (Rails-style) while the payment-session contract is hot through 6.0/6.1, to be relaxed to ">= #{s.version}" (the spree_emails pattern) once it stabilizes. Gateway-only fixes ship as a whole-train patch release.
  4. Optional gems. The meta spree gem does not depend on them; host apps opt in via the Gemfile (starter already lists both). The spree_admin dependency is dropped — 6.0 gateway configuration goes through the Admin API / dashboard.
  5. Each gem hard-depends on its own SDK (stripe, currently >= 10.1, < 19; adyen-ruby-api-library, currently >= 10.3, < 12). SDK constraints live in the gateway gemspec, never in core.
  6. Only the v3 path is ported; use_legacy_* toggles are removed. The 6.0 gems ship a single code path. The drop-set (see Design Details) stays in the old repos.
  7. Spree::PayoutProvider::StripeConnect lives in spree/stripe, next to the gateway and the SDK it requires (marketplace plan Phase 3b).
  8. Old repos become 5.x maintenance branches. Critical fixes only; no new feature work outside the monorepo.
  9. Extraction stays cheap by construction. If a gateway ever moves to community ownership (Vendure's endgame), the per-gem structure lets it leave as a directory move with names, namespaces, and STI data untouched. Folding into core would foreclose this.

Design Details

Monorepo layout

spree/
  core/       spree_core
  api/        spree_api
  emails/     spree_emails      (optional, depends on spree)
  stripe/     spree_stripe      (optional, depends on spree + stripe SDK)
  adyen/      spree_adyen       (optional, depends on spree + adyen-ruby-api-library)

Each gateway gem is a Rails engine following the existing optional-gem shape: version from spree/core/lib/spree/core/version.rb, engine initializer registering the gateway (config.spree.payment_methods << SpreeStripe::Gateway), migrations installable via spree:install:migrations, its own RSpec suite (VCR + webmock, as in the source repos).

Keep-set (what moves)

Both source repos already ship the v3 path alongside the legacy one; the port keeps the v3 half.

Stripe (~37 files, ~1,700 LOC):

  • SpreeStripe::Gateway + Gateway::PaymentSessions, Gateway::PaymentSetupSessions, Gateway::PaymentIntents concerns (intents are Stripe-side API objects wrapped by the session flow — no intent AR model exists)
  • Spree::PaymentSessions::Stripe, Spree::PaymentSetupSessions::Stripe (core-table STI subclasses)
  • SpreeStripe::WebhookKey + PaymentMethodsWebhookKey models and their migration (spree_stripe_webhook_keys, join table) — encrypted signing secrets
  • Payment sources (Klarna, Affirm, Alipay, iDEAL, Link, SEPA, bank transfer, Przelewy24, AfterPay), services (CreatePayment, CreateSource, CreatePaymentSession, CreateGatewayWebhooks, RegisterDomain, UpdateCustomer, CompleteOrder), presenters, jobs, the .stripe scopes on PaymentMethod/GatewayCustomer

Adyen (~98 files, ~3,400 LOC, of which 60 are one-per-method payment sources):

  • SpreeAdyen::Gateway + session/setup-session concerns, HMAC-validating parse_webhook_event, management-API auto-configuration (webhook setup, HMAC/client key generation, allowed origins)
  • Spree::PaymentSessions::Adyen, Spree::PaymentSetupSessions::Adyen (core-table STI subclasses)
  • Event processors (authorisation/capture/cancellation), webhook services, payment sources, presenters, jobs. Adyen stashes PSP references via Spree::Metafields on the payment.

Drop-set (stays in the old repos)

  • Classic Rails storefront: all app/javascript/, storefront ERB partials, checkout/store controller decorators and helpers, redirect/confirm-payment controllers, Apple Pay domain-verification file routes
  • Legacy webhook paths: the stripe_event engine mount + subscriber block and its handler services; Adyen's POST /adyen/webhooks controller and the config.spree_adyen event-code → handler registry
  • SpreeAdyen::PaymentSession standalone model and its spree_adyen_payment_sessions table + migrations (superseded by the core-table Spree::PaymentSessions::Adyen)
  • Adyen's Storefront API v2 tree (lib/spree_api_v2/) and conditional routes
  • Assorted AR decorators (order/payment/store/custom_domain/user) tied to the legacy flows
  • All use_legacy_* preferences and both branches of their conditionals

The Stripe Tax calculator (SpreeStripe::Calculators::StripeTax) is ported only if it fits the 6.0-tax-provider.md direction (see Open Questions).

Registration and admin surface

Nothing new. Gateways register on Rails.application.config.spree.payment_methods in their engine initializer; GET /api/v3/admin/payment_methods/types derives availability from loaded providers, so "gem in Gemfile" = "type offered in admin" with no conditional-SDK logic. Webhook endpoints are the core v3 ones (/api/v3/webhooks/payments/:payment_method_id); Gateway#webhook_url and #parse_webhook_event already target them.

Migration Path

  1. Port Stripe into spree/stripe (smaller keep-set; sequenced first because marketplace Phase 3b builds PayoutProvider::StripeConnect on it). Bring the spec suite + cassettes; wire the gem into monorepo CI and the release train; add it to the starter/dev-server path mounts.
  2. Port Adyen into spree/adyen the same way.
  3. Marketplace Phase 3b adds Spree::PayoutProvider::StripeConnect inside spree/stripe.
  4. Old repos flip to maintenance mode: README banner pointing at the monorepo, 5.x branch for critical fixes, no 6.0 release cut from them.
  5. Host apps upgrade by bumping the gem version — same gem 'spree_stripe' / gem 'spree_adyen' lines, now resolved from the 6.0 release train. No STI data migration. Apps still on the legacy storefront/webhook paths must complete the v3 migration (the use_legacy_* off state) before 6.0, per the gems' existing 5.x migration guides.

Constraints on Current Work

  • No new feature work in the standalone spree_stripe/spree_adyen repos — critical fixes only. New gateway capability lands in the monorepo port.
  • Don't rename the SpreeStripe/SpreeAdyen namespaces anywhere (docs, examples, seeds): STI columns depend on them (Key Decision 2).
  • Changes to the core payment-session contract (Spree::PaymentMethod session methods, Spree::PaymentSession, Spree::Payments::HandleWebhook, the v3 session/webhook endpoints) must be checked against both gateway implementations — after the port, monorepo CI does this; until then, coordinate with the external repos before merging.
  • New payment gateways (e.g. spree_paypal_checkout) follow this plan's shape: own spree/<provider> gem, existing name kept if published, lockstep version, exact spree pin, SDK as the gem's own dependency.
  • The spree meta gem must not gain a dependency on any gateway gem.

Open Questions

  • Webhook-key tables → encrypted preferences? Stripe's two custom tables exist to hold per-endpoint signing secrets; they may collapse into encrypted preferences on the payment method. Decide during the port — default is to keep the tables (smaller diff, proven).
  • Stripe Tax calculator: port into spree/stripe, or drop and revisit as a TaxProvider under 6.0-tax-provider.md? Leaning drop-and-revisit.
  • spree_paypal_checkout timing: same treatment, but in-scope for 6.0 or after? Not blocking this plan.
  • CI secrets for gateway suites: the source repos use VCR cassettes (offline-safe); confirm no live-credential jobs are needed in monorepo CI.

References

  • docs/plans/decisions.md — 2026-07-20 entry (this plan; amends 2026-07-15 "likely into spree/core")
  • docs/plans/6.0-multi-vendor-marketplace.md — Phase 3b (StripeConnect payout provider), OSS/Enterprise boundary
  • docs/plans/6.0-tax-provider.md — Stripe Tax open question
  • Source repos: spree/spree_stripe, spree/spree_adyen
  • Vendure payments extraction: vendurehq/community-plugins (v3.6.0 removal of packages/payments-plugin)
  • Medusa v2 provider packaging: medusajs/medusapackages/modules/providers/payment-stripe (@medusajs/payment-stripe)