The two steps
A consumer installing your plugin needs to:- Install the npm package —
pnpm add @your-scope/your-plugin, then restart the dev server - Apply any backend changes — usually adding a Rails gem, running migrations
package.json declares the spree.dashboard.plugin marker (the CLI scaffold does this for you — see Publishing), spreeDashboardPlugin() walks the host’s dependencies at build time and finds it automatically. Discovery feeds two things:
- Activation — the app entry imports
virtual:spree-dashboard-plugins(the dashboard app shell and the starter template already do), a module the Vite plugin synthesizes with one side-effect import per discovered plugin. YourdefineDashboardPlugincall runs before first render without the host touchingmain.tsx. - Tailwind scanning — your plugin’s class names compile without any extra wiring.
Gemfile — vet your dependencies. Hosts that want explicit control use the whitelist below.
Plugins that take configuration (the definePlugin(config) factory pattern — see below) are the exception: the factory call is host code, so the host imports and invokes it in main.tsx themselves.
Manual whitelist (advanced)
For tightly controlled environments (a host that wants explicit allowlist semantics, or a plugin you want to load conditionally), pass the names explicitly:plugins: [] disables auto-discovery entirely — useful if you want to ship a vanilla dashboard with zero third-party Tailwind sources.
Dev-mode banner
If the consumer registers your plugin explicitly but it’s not installed (or the spelling is off),@spree/dashboard-core/vite shows a Vite error overlay during dev with the package name and a fix-it checklist. The same overlay fires if a discovered plugin can’t be resolved on disk. That replaces the older silent-failure mode where missing plugins emitted unstyled HTML.
What to put in your README
A README that gets users to “working in five minutes” has:- A one-paragraph “what this does”
- Screenshots or a short clip of the dashboard UI it adds
- The four install steps (literally copy-paste blocks)
- A “configuration” section if the plugin has runtime config
- A “compatibility” table (
@spree/dashboard >=6.0.0,Spree >=6.0.0, etc.) - Links to the source for slots/components the plugin overrides
Configuration patterns
Plugins occasionally need runtime config — API endpoints, feature toggles, custom permissions. Two patterns:Module-level configure call
defineDashboardPlugin call) always executes before your configure(...) line runs. This pattern therefore only works for values the plugin reads lazily — at render or fetch time, like an API endpoint — never for anything its import-time registration depends on. If config shapes what gets registered (which nav entries, which routes), use the factory pattern below instead.
definePlugin(config) factory
definePlugin as the way to install.
Upgrades
When you cut a major version of your plugin, the breaking-changes section of your changelog needs to call out:- Renamed registry keys (
nav.add({ key: ... })changes are user-visible — their URLs depend on it) - Locale-key renames (consumers may have overrides keyed off old names)
- Slot ID changes (consumers extending your slots break)
peerDependenciesrange bumps (consumers may need to upgrade Spree first)
configure / definePlugin API. Everything else is internal.
Coexistence with other plugins
The registries are global singletons, so a key collision is the consumer’s problem to debug. Mitigations:- Namespace your keys —
key: 'acme-brands'notkey: 'brands'. - Namespace locale keys —
admin.acme_brands.titlenotadmin.brands.title. - Namespace API paths — your Rails engine should mount under
/api/v3/admin/acme/..., not/api/v3/admin/brands(which collides with core Brands if it ever ships).
Distribution channels
- npm public — what most plugins want.
pnpm publish --access public. - npm private (paid) — for commercial plugins; combine with a license key checked at runtime.
- GitHub Package Registry — internal-only; consumers add
@your-scope:registry=https://npm.pkg.github.comto.npmrc. - Git tarball —
pnpm add github:your-org/your-plugin#mainworks for unpublished plugins, useful while iterating against a single consumer.
next dist-tag (pnpm publish --tag next) so pnpm add @your-scope/your-plugin resolves to the stable line and pnpm add @your-scope/your-plugin@next opts in.
Reference
- Example consumer wiring — what
spreeDashboardPluginlooks like in production - Plugins reference (
brandsexample) — what a finished plugin looks like end-to-end

