Architecture
One boot sequence, one config store, and four engines that listen to it.
Boot order
src/main.ts runs in this order, and the order matters at three points.
Create the app and install Pinia
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)The persistence plugin goes on before any store is used, so useConfig rehydrates from localStorage the first time it is touched.
Wire the config to the engines
setupConfigWiring()Every watcher inside is immediate: true, so the theme ramp, the fonts, the density, the radius, the locale and the direction are applied before anything paints. A persisted brand color is on screen at first paint, not one frame later.
Bootstrap the session, then install the router
import('@modules/auth/composables/useAuth').then(({ bootstrapAuth }) => {
return bootstrapAuth()
}).finally(() => {
app.use(router)
})This is the first order that matters. app.use(router) runs the initial navigation synchronously, and the guard reads the auth store. Install the router first and every reload bounces a signed-in user to the sign-in page.
bootstrapAuth calls /auth/me with _skipAuthRefresh, because a 401 there is the normal answer for a signed-out visitor, not an expired session. On success it fills the store and rebuilds the CASL ability. Either way it then asks for the tenant theme, without waiting for it.
Install the ability, the plugins and the error handler
app.use(abilitiesPlugin, ability, { useGlobalProperties: true })
installPlugins(app)
setupErrorHandler(app)installPlugins adds vue-i18n, points the schema engine's validation messages and placeholders at the translation files, starts the direction watcher, and registers the two global directives, v-permission and v-copy.
setupErrorHandler sets app.config.errorHandler and an unhandledrejection listener. Both log to the console and carry a commented Sentry call where a production hook belongs.
Register the shortcuts
? opens the cheat sheet, g d g s g p go to the dashboard, settings and profile, t s toggles the sidebar and t c the settings drawer. Ctrl + K is a plain listener rather than a binding, because it has to call preventDefault to beat the browser's own and must work before the palette's chunk arrives.
Mount
app.mount('#app')How the pieces connect
useConfig (Pinia store, persisted as "sa-config")
│
setupConfigWiring() one watcher per section, all immediate
│
┌────────────┼────────────┬─────────────┬──────────────┐
useTheme useFonts useTypography useLocale useDirection
useDensity
│ │ │ │ │
--sa-* ramps @font-face --sa-text-* messages dir + lang
on <html> per slot scale vars per locale on <html>The rule behind it: components change config values, never engine state. A component sets config.theme.primary, and the watcher calls setPrimary. Nothing calls setPrimary itself, so the persisted config and what is on screen cannot disagree.
Config
useConfig is a Pinia store holding one SmartAdminConfig object, persisted under the key sa-config. It is value-only: mergeConfig, resetConfig, setFeature, setUi, and no engine calls. Full reference: Global config.
Theme
useTheme is a module-scope singleton. setPrimary, setSecondary and setAccent each turn one hex into an eleven-stop ramp and write --sa-primary-50 through --sa-primary-950 onto <html>. Dark mode is useColorMode from VueUse, toggling the dark class and storing the choice under sa-color-mode. Details: Theming.
Internationalization
Nine locales live in src/locales. English is the base and is bundled; the other eight are dynamic imports, loaded when the language is chosen. Direction follows the locale unless ui.direction pins it, which is what lets the settings drawer preview a right-to-left layout without switching language. Details: Internationalization.
Permissions
/auth/me returns a flat permission list. updateAbility rebuilds the CASL ability from it, and three things read that ability: the route guard through meta.permission, the sidebar through NavItem.permission, and the v-permission directive. Details: Permissions.
Routing
Routes are files. unplugin-vue-router scans src/modules/*/pages/**, and each page declares its own path, name and meta in a <route> block. On top of the generated table, src/app/router/index.ts adds the root redirect to /dashboard, the profile, plans and FAQ pages, the offline page, a linkable /404 and the catch-all.
Two more things live there. scrollBehavior scrolls the layout's own column rather than the window, because the window never scrolls in this shell, and it remembers each route's offset so the browser back button lands where the reader left. And every named route that has a meta.title is registered as a command, which is how Ctrl + K can navigate to any page without a list being maintained by hand.
What runs outside the router
App.vue holds what has to survive a route change: the resolved layout, the toaster, the confirm dialog host, the command palette, the shortcut cheat sheet, the tour spotlight, the offline banner and the settings drawer. They are mounted once, above <RouterView>, so they work on the sign-in pages too.
Where to go next
- Global config — every key, its default and its effect.
- Schema — the field list that drives forms and tables.
- Permissions — roles, routes and the directive.