Project structure
Four folders under src carry the whole app, and each one has a job.
The tree
smart-admin/
├── cli/ The smart-admin command and its templates
├── public/ Files copied to the build as they are
├── scripts/ The i18n checks, the dev API stub, the demo avatar script
├── src/
│ ├── app/ Router, guards, plugins, the global error handler
│ ├── assets/styles/ main.css and the per-locale font rules
│ ├── core/ The systems: config, crud, editor, fonts, i18n, schema, theme
│ ├── layouts/ The four shells, the nav tree and the shell components
│ ├── locales/ Nine translation files, one per language
│ ├── modules/ Feature modules, one folder each
│ ├── shared/ Components, composables, directives, lib, types, utils
│ ├── views/ Pages that belong to no module
│ ├── App.vue The root: layout, toaster, palette, customizer
│ └── main.ts Boot order
├── .env.development The demo backend and the pre-filled sign-in, for npm run dev
├── .env.example The API, auth, realtime and demo variables, explained
├── Dockerfile Production image: build, then nginx
├── nginx.conf SPA fallback, security headers, compressed assets
└── vercel.json The one rewrite a Vercel deploy needsWhat each folder is for
src/core
The systems the rest of the app builds on, each in its own folder: config (the SmartAdminConfig shape, its store and the wiring that applies it), crud (useCrud), editor, fonts, i18n, schema (FieldDefinition, SchemaForm, SchemaTable, the Zod generator) and theme (the color ramp generator, density, typography).
Nothing in core imports a module.
src/shared
What more than one feature needs. components/ holds ui/ (the shadcn-vue primitives), common/, feedback/, pages/ (the page layouts) and backgrounds/. Beside it: composables/, directives/ (v-copy, v-permission), lib/ (the axios instance, the CASL ability, the auth mock), types/ and utils/.
src/modules
One folder per feature. A shipped CRUD module looks like this:
src/modules/product/
├── api/ The HTTP calls for this entity
├── composables/ Its own composables
├── pages/ index.vue, new.vue, [id].vue, [id].edit.vue
├── stores/ A Pinia store, when the module has one
├── types/ The TypeScript types and the field schema
├── __mock__/ Demo data, inert unless the mock is on
└── index.ts Side-effect entry: registers command-palette entriesThe generator writes a smaller version of this — the entry, the list page and the types — and leaves the API call, the store and the permissions to you, because it cannot know them. See Add a module.
There is no locales/ folder inside a module. Every string lives in src/locales/*.json under the module's own namespace, which is what lets check:i18n see the whole app at once.
pages/ is where the routes come from. unplugin-vue-router scans src/modules/*/pages/**, and each page declares its own URL in a <route> block at the end of the file:
<route lang="json">
{
"path": "/products/:id/edit",
"name": "products-edit",
"meta": { "title": "products.edit.title", "breadcrumb": "common.crumb.edit", "permission": "update Product" }
}
</route>A new page is a new file plus that block. The router picks it up when the dev server restarts.
src/layouts
ClassicLayout, ModernLayout, MinimalLayout and HorizontalLayout, plus AuthLayout for the sign-in pages. navItems.ts is the sidebar tree, useLayout.ts resolves which shell a route gets, and components/ holds the navbar, sidebar, footer, the mobile drawer and the horizontal menu.
src/views
The five pages that belong to no feature: the profile, the plans page, the FAQs, the 404 and the offline page.
Path aliases
Five aliases, declared in vite.config.ts and mirrored in tsconfig.app.json:
| Alias | Points at |
|---|---|
@/ | src/ |
@core/ | src/core/ |
@shared/ | src/shared/ |
@modules/ | src/modules/ |
@app/ | src/app/ |
Use them everywhere. A relative import that climbs out of its own folder is a sign something belongs somewhere else.
import router from '@app/router'
import { useConfig } from '@core/config/useConfig'
import { api } from '@shared/lib/axios'One rule keeps this honest
A module may import from @core, @shared, @app and its own folder. It does not reach into another module's api/, stores/, pages/ or composables/.
The rule is a convention, not a lint error, so it is worth knowing where the shipped modules bend it: a few demo modules import a sibling's types for a related entity, such as hr-attendance reading HrEmployeeRef from @modules/hr-employee/types. That is the only crossing in the whole tree, and it stays inside types/. Nothing imports a sibling's store or its API.
Keeping to it means a module is a folder you can copy into another project, or delete in one command:
rm -rf src/modules/campaignThen remove its two lines from src/main.ts and its entry from src/layouts/navItems.ts.
Where to go next
- Architecture — what happens between
main.tsand the first paint. - Add a module — generate one of these folders.