Tech stack at a glance
The dashboard is a Single Page Application: one HTML page, all routing client-side. No Rails views, no Turbo, no server-rendered partials. Everything the user sees comes from React components fetching JSON from
/api/v3/admin/*.
The three-package split
-ui has no providers or hooks (data comes via props); -core is the extension API and runtime services; -dashboard is the deployable app shell. Plugins and customizations consume -core and -ui, never the app shell.
How customization plugs in
Five global, in-memory registries hold the things you add or modify:
Each is a module singleton with mutators to add, remove, and patch entries (nav can also insert relative to an existing entry or nest children under one). The consumer components subscribe to changes, so registering late still updates the UI on the next render.
The facade
defineDashboardPlugin({ nav, routes, slots, … }) groups all five into one declarative call — convenient for plugins, optional for in-app customizations (you can call nav.add() directly).
Auth, context, providers
The signed-in admin’s identity and abilities are exposed via three providers wrapping the app:<AuthProvider>— the current admin user;useAuth()returns{ user, signIn, signOut, … }<PermissionProvider>— CanCanCan abilities;usePermissions()returns{ can, cannot }<StoreProvider>— current store + timezone + currency;useStore()returns{ storeId, store, … }
Data fetching
Every screen in the dashboard follows the same pattern:src/hooks/, never call adminClient directly from components. See Backend integration for the full pattern, error mapping, and the useResourceMutation helper that wires up 422 handling.
URL = state
The dashboard treats the URL as the single source of truth for page state:- Filters, sort, page number → URL search params
- Selected row → URL path param
- “Sheet open?” → URL search param (
?edit=prod_xxx)
i18n is not optional
The dashboard ships with English, German, French, Polish, Arabic, and Simplified Chinese out of the box. Every label, placeholder, error message goes throughi18n.t(). When you add a feature:
- Add the keys to your customization’s
locales/en.json - Register the bundle:
i18n.addResourceBundle('en', 'translation', bundle, true, true) - Use
useTranslation()in components andi18n.t()at registration sites
Prefixed IDs everywhere
The Admin API uses Stripe-style prefixed IDs:prod_86Rf07xd4z, cust_k5nR8xLq. The dashboard does too — URLs, hook params, table rows, SDK calls. Never coerce IDs to integers. Never strip the prefix. Pass them around as opaque strings.
Where to go next
- Public API — what’s importable from
@spree/dashboard-core/-ui/-dashboard - Customization Quickstart — first nav entry in 30 seconds
- Recipes — focused walkthroughs for common extension shapes

