docs/developer/contributing/developing-spree.mdx
Please read our Code of Conduct before contributing.
Spree is a big monorepo. For a faster clone, use a partial clone which downloads file contents on demand while keeping full commit history for git log and git blame:
git clone --filter=blob:none https://github.com/spree/spree.git
You need Node.js 20+ to run the workspace scripts (including pnpm server:setup, which both backend and TypeScript contributors use). Install it via nvm, mise, fnm, or your package manager (brew install node on macOS).
pnpm is provisioned automatically via Corepack (bundled with Node) — the repository pins its version in package.json, and the first pnpm command will fetch the matching release. If Corepack is disabled in your environment, run corepack enable once.
Spree is a monorepo with three main areas:
spree/ — Ruby gems (core, api, admin, emails) distributed as separate packages via RubyGemspackages/ — TypeScript packages (SDKs, CLI, project scaffolding, docs, React dashboard)server/ — A Rails application cloned from spree-starter that mounts the local Spree gems (not checked in — pnpm server:setup creates it)A single command bootstraps the whole monorepo for both backend and frontend work. You need Docker running locally (Docker Desktop, OrbStack, or any compatible runtime) — no Ruby, Postgres, or Redis on your host.
pnpm install # workspace dependencies
pnpm server:setup # ~5–10 min on first run; idempotent
pnpm server:setup clones spree-starter into ./server/, wires it to load Spree gems from the monorepo via a Docker compose overlay, builds the dev image, starts the stack (Postgres + Redis + Meilisearch + Rails web + Sidekiq worker), and prepares the database. The full sequence lives in scripts/server-setup.sh.
When it's done, the backend is up at http://localhost:3000 and the admin is at http://localhost:3000/admin. Sign in with the seed admin: [email protected] / spree123 (override at seed time with ADMIN_EMAIL / ADMIN_PASSWORD env vars — see spree/core/app/services/spree/seeds/admin_user.rb).
Optionally load sample products, taxonomies, and option types:
pnpm server:load_sample_data
After the one-time setup, use these to bring the stack up and down:
pnpm server:dev # run the stack in the foreground — streams web + worker logs, Ctrl+C stops them
pnpm server:stop # full teardown (also stops postgres / redis / meilisearch)
pnpm server:restart # restart web + worker in place
pnpm server:logs # follow web container logs (when the stack runs detached)
pnpm server:console # open a Rails console inside the container
pnpm server:seed # re-run database seeds
pnpm server:build # rebuild the dev image (only after Dockerfile / .ruby-version changes)
server:dev behaves like any TS dev server (vite dev, the dashboard's pnpm dev): it runs in the foreground and Ctrl+C stops the app containers. Postgres, Redis, and Meilisearch keep running for a fast next boot — pnpm server:stop shuts everything down.
Run any CLI command against the running backend from server/:
cd server
pnpm exec spree migrate
pnpm exec spree generate model Brand name:string
pnpm exec spree upgrade --plan
See the @spree/cli README for the full command surface.
Which command after which change:
| What changed | What to run |
|---|---|
Ruby code in spree/* gems | Nothing — gems are bind-mounted; code reloads on the next request |
| A new migration in a gem | Nothing — the next pnpm server:dev boot applies it (or cd server && pnpm exec spree migrate while the stack runs) |
Gem dependencies (gemspec / Gemfile / lock drift after git pull) | Nothing — the next pnpm server:dev boot self-heals via `bundle check |
Compose files / server/.env | pnpm server:dev (force-recreates the containers) |
server/Dockerfile / .ruby-version / starter update that breaks the image build ("lockfile can't be updated because frozen") | pnpm server:build, then pnpm server:dev — the build script handles the edge-rewritten Gemfile.lock automatically |
| Meilisearch image bump ("database version … is incompatible") | Remove the server_meilisearch_data volume, boot, then reindex (pnpm exec spree rake spree:search:reindex) — the index is derived data |
| Broken beyond repair | pnpm server:setup (full reset — wipes the database and volumes) |
Re-run pnpm server:setup only to fully reset — it does docker compose down -v + rm -rf ./server, wiping all DB data.
If you prefer the fastest possible inner loop and don't mind installing Ruby, Postgres, Redis, and Meilisearch on your host directly, you can skip Docker:
pnpm server:create # clones spree-starter, writes server/.env with SPREE_PATH=..
cd server
bin/setup # installs Ruby (via mise), Postgres/Redis/Meilisearch (via brew bundle on macOS), gems, prepares the database
bin/dev # starts Rails + Sidekiq + CSS watchers via Foreman
This path is faster per request but means more on your host. It also runs against your installed system services, not a sandboxed Docker stack.
The Spree Rails engines live inside spree/ and are distributed as separate gems (Ruby packages installed via Bundler):
| Engine | Gem | Description |
|---|---|---|
core | spree_core | Models, services, business logic |
api | spree_api | REST APIs |
admin | spree_admin | Admin dashboard |
emails | spree_emails | Transactional emails |
All Spree models, controllers and other Ruby classes are namespaced by the Spree keyword, eg. Spree::Product. This means that those files are also located in spree sub-directories eg. app/models/spree/product.rb.
Each engine has its own test suite. First install the shared dependencies at the spree/ level, then navigate into the specific engine to set up and run its tests:
# 1. Install shared dependencies
cd spree
bundle install
# 2. Set up and run tests for a specific engine (e.g. core)
cd core
bundle install
bundle exec rake test_app
bundle exec rspec
Replace core with api, admin, or emails to test other engines.
By default engine tests run against SQLite3. To run against PostgreSQL, set the DB environment variable:
DB=postgres DB_USERNAME=postgres DB_PASSWORD=password DB_HOST=localhost bundle exec rake test_app
Run a single spec file:
cd spree/core
bundle exec rspec spec/models/spree/state_spec.rb
Run a specific test by line number:
cd spree/core
bundle exec rspec spec/models/spree/state_spec.rb:7
For faster test runs on multi-core machines, you can use the parallel_tests gem to distribute spec files across multiple CPU cores.
After setting up the test app, create databases for parallel workers:
cd spree/core
bundle exec rake parallel_setup
Then run specs in parallel:
bundle exec parallel_rspec spec
You can also specify the number of workers:
bundle exec rake "parallel_setup[4]"
bundle exec parallel_rspec -n 4 spec
After schema changes, re-run bundle exec rake parallel_setup to update the worker databases.
The legacy Rails admin (spree/admin) ships feature specs that run in a real browser via chromedriver. You only need this if you're touching the legacy admin UI.
Install chromedriver on macOS:
brew install chromedriver
The 6.0 React dashboard (packages/dashboard) has its own end-to-end test suite running on Playwright against a real Rails backend — see Dashboard E2E tests under TypeScript Development.
Spree runs slower in development because caching is disabled and code reloads on each request. To turn on caching:
cd server && pnpm exec spree rails dev:cache
Restart the Rails server after running this (Ctrl+C the running pnpm server:dev and start it again).
The backend setup is the same as for Ruby work — see Setup above. With pnpm server:setup done, the Rails backend is running and you can start the packages you want to work on.
| Package | Path | Published | Description |
|---|---|---|---|
@spree/sdk | packages/sdk | yes | TypeScript SDK for the Spree Store API |
@spree/admin-sdk | packages/admin-sdk | yes | TypeScript SDK for the Spree Admin API |
@spree/cli | packages/cli | yes | CLI for managing Spree Commerce projects |
create-spree-app | packages/create-spree-app | yes | Project scaffolding (npm create spree-app) |
@spree/docs | packages/docs | yes | Developer documentation for AI agents and local reference |
@spree/dashboard | packages/dashboard | no | React SPA admin dashboard (Spree 6.0, replaces the legacy Rails admin) |
@spree/dashboard-core | packages/dashboard-core | no | Framework: registries, providers, generic infra hooks for the dashboard |
@spree/dashboard-ui | packages/dashboard-ui | no | Design system used by the dashboard |
@spree/sdk-core | packages/sdk-core | no | Shared HTTP/retry/error layer used by both SDKs |
Run from the repository root — Turborepo orchestrates tasks across all packages:
| Command | Description |
|---|---|
pnpm dev | Watch mode for all packages (does not boot the backend — use pnpm server:dev for that) |
pnpm build | Build all packages (Turbo resolves dependency order) |
pnpm test | Run tests in all packages |
pnpm lint | Lint all packages |
pnpm typecheck | Type-check all packages |
pnpm clean | Remove build artifacts |
You can also run commands in a single package:
pnpm --filter @spree/sdk test:watch
pnpm --filter @spree/sdk console
pnpm --filter @spree/sdk generate:zod
Tests use Vitest with MSW for API mocking at the network level.
The dashboard runs as a Vite dev server (port 5173) that proxies /api/* to the Rails backend on port 3000 — required for the auth refresh cookie to work over plain HTTP.
# Terminal 1: backend
pnpm server:dev
# Terminal 2: dashboard
cd packages/dashboard
pnpm dev # http://localhost:5173
Sign in with [email protected] / spree123. To point at a non-default backend, set VITE_SPREE_API_URL (also needs to be set at build time for production bundles, not just dev):
VITE_SPREE_API_URL=https://my-spree.example.com pnpm dev
Gotcha: the dashboard imports @spree/admin-sdk from its built dist/, not source. If you edit admin-sdk (or sdk-core, or sdk) and want the dashboard to see the changes, run pnpm build from the monorepo root first (Turbo-cached). Editing dashboard code alone doesn't need this.
See the dashboard README for the full architecture (auth, permissions, extension points, the three-package split between dashboard, dashboard-core, and dashboard-ui).
The React dashboard ships an end-to-end suite running on Playwright against a real Rails backend — no mocks. The spec files exercise the full UI: login, staff invitation, invitee signup. CI runs the same suite via the dashboard-e2e job in .github/workflows/packages.yml.
To run locally you need Ruby (the suite boots a real Rails server) and a one-time browser install:
# Make sure the dummy Rails app exists (one-time, after a fresh checkout).
cd spree/api
bundle install
bundle exec rake test_app
# Install Playwright's Chromium (one-time per machine).
cd ../../packages/dashboard
pnpm test:e2e:install
# Run the suite. Boots Rails on :3010 + Vite on :5174, runs all specs, tears
# both down. Safe to re-run repeatedly — the SQLite DB resets each run.
pnpm test:e2e
For interactive debugging (time-travel through actions, inspect the DOM at each step):
pnpm test:e2e:ui
Filter to a single spec or test name:
pnpm test:e2e e2e/auth.spec.ts
pnpm test:e2e -g "invites a teammate"
When a test fails, Playwright drops a screenshot, video, and DOM snapshot in packages/dashboard/test-results/<spec>/ — usually enough to diagnose without re-running.
TypeScript types in packages/sdk/src/types/generated/ (Store API) and packages/admin-sdk/src/types/generated/ (Admin API) are auto-generated from the Rails serializers using typelizer. Zod schemas in packages/sdk/src/zod/generated/ are derived from the TS types.
A Lefthook pre-commit hook regenerates both whenever you commit a change to spree/api/app/serializers/**/*.rb and re-stages the generated output. You don't need to run anything manually for the common case.
To regenerate manually (useful when iterating on serializers before committing):
cd server
pnpm exec spree rake typelizer:generate # regenerate TS types
cd ../packages/sdk
pnpm generate:zod # regenerate Zod schemas
If you'd rather avoid the Docker round-trip and have Ruby on your host, the native equivalent works too:
cd spree/api
bundle install
bundle exec rake typelizer:generate
Published packages use Changesets for version management. Each package owns its own .changeset/ folder, so changesets must be created from inside the package directory:
cd packages/sdk # or packages/admin-sdk, packages/cli, packages/create-spree-app
pnpm changeset
This creates a changeset file describing your changes. Commit it with your PR.
Releasing is a two-step flow: a maintainer consumes the pending changesets (changeset version — writes the CHANGELOG and bumps package.json) and merges that bump to main; the release jobs in .github/workflows/packages.yml then detect the unpublished version and publish with npm provenance via Trusted Publishing. @spree/admin-sdk publishes under the next dist-tag while on the 0.x Developer Preview line; the others ship as latest (prereleases go to beta).
Private packages (@spree/dashboard, @spree/dashboard-core, @spree/dashboard-ui, @spree/sdk-core) don't need changesets.
Consistent code style is enforced via automated linters. Please make sure your changes pass linting before submitting a PR.
Ruby: We use RuboCop for Ruby code. The configuration lives in server/.rubocop.yml and is shipped with spree-starter, so it's only available after running pnpm server:setup. Run it inside the container via the CLI:
cd server
pnpm exec spree exec bundle exec rubocop
To auto-fix correctable offenses:
pnpm exec spree exec bundle exec rubocop -a
(On the native path, drop the pnpm exec spree exec prefix and run bundle exec rubocop directly.)
TypeScript: We use Biome for linting and formatting TypeScript code. Run it from the repository root:
pnpm lint
Create a new branch for your changes. Do not push changes to the main branch. Branch names should be human-readable and informative:
fix/order-recalculation-total-bugfeature/my-new-amazing-featureKeep your commit history meaningful and clear. Each commit should represent a logical unit of work. This guide covers this well.
A few tips:
Fixes #<number> in the commit message or PR description to auto-close it on mergeWe use GitHub Actions to run CI.
Push your changes to a topic branch in your fork of the repository.
git push origin fix/order-recalculation-total-bug
Create a Pull Request against the main branch.
Wait for CI to pass.
Wait for Spree Core team code review. We aim to review and leave feedback as soon as possible.
To help us review your PR quickly:
@spree/sdk, @spree/admin-sdk, @spree/cli, or create-spree-app). Run pnpm changeset from inside that package's directory — each package owns its own .changeset/ folder.We use GitHub Issues to track bugs. Before filing a new issue, please search existing issues to avoid duplicates.
When reporting a bug, please include:
We have an issue template that will guide you through this.
Issues that are open for 14 days without actionable information or activity will be marked as stale and then closed. They can be re-opened if the requested information is provided.
Spree comes with an AGENTS.md file that instructs coding agents like Claude Code or Codex to help you with your development.
We also ship agent skills (npx skills add spree/agent-skills), a docs MCP server, and LLM-ready documentation — see the Agentic Development docs for setup across Claude Code, Cursor, Copilot, and other tools.
The MCP server URL for quick setup:
https://spreecommerce.org/docs/mcp
Thank you for participating in Open Source and improving Spree - you're awesome!