Tech stack at a glance
| Layer | What we use | Why it matters |
|---|---|---|
| Build | Vite | Fast HMR, dev server, source consumption of TS/TSX |
| UI runtime | React 19 | Components, hooks, suspense |
| Routing | TanStack Router (file-based) | Type-safe URLs, nested layouts, search params |
| Data | TanStack Query v5 | Caching, refetch, mutation tracking |
| Forms | React Hook Form + Zod | Uncontrolled inputs + schema validation |
| Styling | Tailwind v4 + shadcn/ui + Base UI | Utility classes + headless primitives |
| i18n | i18next | Translations, fallback chains |
| Lint/format | Biome | ESLint + Prettier in one tool |
| HTTP | @spree/admin-sdk | Typed Admin API client |
/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:| Registry | Adds | Surfaced by |
|---|---|---|
nav | Sidebar entries | <AppSidebar> |
settingsNav | Settings sub-shell entries | <SettingsSidebar> |
| Route registry | Custom pages mounted under /$storeId/* | Catch-all dispatcher route |
| Slot registry | Components injected into named slots in built-in pages | <Slot name="..."> |
| Table registry | Columns (and their filters) on built-in list tables | <ResourceTable> |
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

