@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/.cursor/mcp.json +8 -0
  2. package/.mcp.json +8 -0
  3. package/.prettierignore +20 -0
  4. package/.vscode/mcp.json +8 -0
  5. package/AGENTS.md +432 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +481 -0
  10. package/cli/plugins.ts +14 -0
  11. package/client/agent-annotations-host.ts +19 -0
  12. package/client/app.ts +38 -0
  13. package/client/components/back-button.tsx +49 -0
  14. package/client/components/breadcrumbs.tsx +167 -0
  15. package/client/components/inbox-badge.ts +49 -0
  16. package/client/components/inbox-button.tsx +132 -0
  17. package/client/components/inbox-header-button.tsx +25 -0
  18. package/client/components/loading.tsx +35 -0
  19. package/client/components/page-container.tsx +24 -0
  20. package/client/components/page-header.tsx +33 -0
  21. package/client/components/route-child-page.tsx +121 -0
  22. package/client/components/route-dialog.tsx +10 -0
  23. package/client/components/route-drawer.tsx +10 -0
  24. package/client/components/route-overlay.tsx +190 -0
  25. package/client/components/ui/alert.tsx +75 -0
  26. package/client/components/ui/badge.tsx +51 -0
  27. package/client/components/ui/breadcrumb.tsx +121 -0
  28. package/client/components/ui/button.tsx +58 -0
  29. package/client/components/ui/collapsible.tsx +19 -0
  30. package/client/components/ui/context-menu.tsx +271 -0
  31. package/client/components/ui/dialog.tsx +160 -0
  32. package/client/components/ui/dropdown-menu.tsx +270 -0
  33. package/client/components/ui/empty.tsx +103 -0
  34. package/client/components/ui/field.tsx +236 -0
  35. package/client/components/ui/input-group.tsx +158 -0
  36. package/client/components/ui/input.tsx +20 -0
  37. package/client/components/ui/kbd.tsx +26 -0
  38. package/client/components/ui/label.tsx +18 -0
  39. package/client/components/ui/popover.tsx +89 -0
  40. package/client/components/ui/select.tsx +200 -0
  41. package/client/components/ui/separator.tsx +22 -0
  42. package/client/components/ui/sheet.tsx +137 -0
  43. package/client/components/ui/sidebar.tsx +723 -0
  44. package/client/components/ui/skeleton.tsx +13 -0
  45. package/client/components/ui/spinner.tsx +20 -0
  46. package/client/components/ui/tabs.tsx +79 -0
  47. package/client/components/ui/textarea.tsx +17 -0
  48. package/client/components/ui/toast.tsx +231 -0
  49. package/client/components/ui/tooltip.tsx +63 -0
  50. package/client/components/use-route-overlay.ts +26 -0
  51. package/client/config/api.ts +13 -0
  52. package/client/config/app.ts +8 -0
  53. package/client/config/auth.ts +18 -0
  54. package/client/config/index.ts +17 -0
  55. package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
  56. package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
  57. package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
  58. package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
  59. package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
  60. package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
  61. package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
  62. package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
  63. package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
  64. package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
  65. package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
  66. package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
  67. package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
  68. package/client/extensions/nocobase-file-component-ui/README.md +13 -0
  69. package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
  70. package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
  71. package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
  72. package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
  73. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
  74. package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
  75. package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
  76. package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
  77. package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
  78. package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
  79. package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
  80. package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
  81. package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
  82. package/client/extensions/nocobase-inbox/frame.ts +31 -0
  83. package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
  84. package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
  85. package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
  86. package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
  87. package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
  88. package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
  89. package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
  90. package/client/extensions/nocobase-inbox/model.ts +428 -0
  91. package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
  92. package/client/extensions/nocobase-inbox/registry.ts +303 -0
  93. package/client/extensions/nocobase-inbox/source.ts +117 -0
  94. package/client/hooks/use-mobile.ts +21 -0
  95. package/client/index.tsx +60 -0
  96. package/client/layouts/app-layout.tsx +105 -0
  97. package/client/layouts/components/app-brand.tsx +49 -0
  98. package/client/layouts/components/app-sidebar.tsx +196 -0
  99. package/client/layouts/components/header-actions.tsx +50 -0
  100. package/client/layouts/components/language-switcher.tsx +95 -0
  101. package/client/layouts/components/layout-header.tsx +17 -0
  102. package/client/layouts/components/navigation-menu.tsx +411 -0
  103. package/client/layouts/components/sidebar-footer.tsx +107 -0
  104. package/client/layouts/components/surface-empty.tsx +35 -0
  105. package/client/layouts/components/user-menu.tsx +127 -0
  106. package/client/layouts/index.ts +2 -0
  107. package/client/layouts/settings-layout.tsx +172 -0
  108. package/client/layouts/use-sidebar-preference.ts +57 -0
  109. package/client/lib/constants.ts +5 -0
  110. package/client/lib/toaster.ts +56 -0
  111. package/client/lib/utils.ts +1 -0
  112. package/client/locales/en-US.ts +178 -0
  113. package/client/locales/index.ts +9 -0
  114. package/client/locales/zh-CN.ts +161 -0
  115. package/client/pages/auth/device.tsx +41 -0
  116. package/client/pages/auth/forgot-password.tsx +39 -0
  117. package/client/pages/auth/login.tsx +66 -0
  118. package/client/pages/auth/register.tsx +46 -0
  119. package/client/pages/auth/reset-password.tsx +38 -0
  120. package/client/pages/auth/shared.tsx +111 -0
  121. package/client/pages/home.tsx +16 -0
  122. package/client/pages/inbox.tsx +12 -0
  123. package/client/plugins.ts +41 -0
  124. package/client/react-providers.ts +27 -0
  125. package/client/route-overrides.ts +9 -0
  126. package/client/routes.ts +75 -0
  127. package/client/routing/app-router.tsx +127 -0
  128. package/client/routing/client-route.tsx +129 -0
  129. package/client/routing/route-context.tsx +85 -0
  130. package/client/routing/route-navigation.ts +153 -0
  131. package/client/routing/route-tree.tsx +42 -0
  132. package/client/routing/standalone-page-layout.tsx +15 -0
  133. package/client/runtime.ts +26 -0
  134. package/client/service-provider.ts +40 -0
  135. package/client/source-extensions.ts +19 -0
  136. package/client/startup.tsx +22 -0
  137. package/client/styles.css +122 -0
  138. package/client/theme/index.ts +4 -0
  139. package/client/theme/theme-context.ts +12 -0
  140. package/client/theme/theme-preferences.ts +84 -0
  141. package/client/theme/theme-presets.ts +7 -0
  142. package/client/theme/theme-provider.tsx +106 -0
  143. package/client/theme/theme-settings.tsx +176 -0
  144. package/client/theme/themes/compact.css +140 -0
  145. package/client/theme/themes/default.css +117 -0
  146. package/client/vite-env.d.ts +5 -0
  147. package/components.json +27 -0
  148. package/config.example.yml +172 -0
  149. package/database/main/migrations/.gitkeep +0 -0
  150. package/database/main/migrations/202610060001_create_device_code.ts +37 -0
  151. package/database/main/seeds/.gitkeep +0 -0
  152. package/database/tsconfig.json +4 -0
  153. package/ecosystem.config.js +35 -0
  154. package/eslint.config.js +24 -0
  155. package/index.html +100 -0
  156. package/package.json +200 -3
  157. package/playwright.config.ts +10 -0
  158. package/public/assets/favicon.ico +0 -0
  159. package/public/assets/logo-dark.png +0 -0
  160. package/public/assets/logo-mark-dark.png +0 -0
  161. package/public/assets/logo-mark.png +0 -0
  162. package/public/assets/logo.png +0 -0
  163. package/server/app.ts +50 -0
  164. package/server/config/ai.ts +27 -0
  165. package/server/config/api.ts +8 -0
  166. package/server/config/app.ts +40 -0
  167. package/server/config/auth.ts +36 -0
  168. package/server/config/authorization.ts +18 -0
  169. package/server/config/caching.ts +21 -0
  170. package/server/config/database.ts +22 -0
  171. package/server/config/drive.ts +30 -0
  172. package/server/config/i18n.ts +17 -0
  173. package/server/config/index.ts +73 -0
  174. package/server/config/jobs.ts +27 -0
  175. package/server/config/logging.ts +23 -0
  176. package/server/config/notification.ts +11 -0
  177. package/server/config/queue.ts +28 -0
  178. package/server/config/scheduler.ts +23 -0
  179. package/server/config/secrets.ts +10 -0
  180. package/server/config/server.ts +26 -0
  181. package/server/config/session.ts +60 -0
  182. package/server/config/snowflake.ts +21 -0
  183. package/server/config/spa.ts +24 -0
  184. package/server/config/users.ts +11 -0
  185. package/server/config/workflow.ts +24 -0
  186. package/server/config.ts +24 -0
  187. package/server/embedded.ts +23 -0
  188. package/server/locales/en-US.ts +10 -0
  189. package/server/locales/index.ts +10 -0
  190. package/server/locales/zh-CN.ts +5 -0
  191. package/server/plugins.ts +41 -0
  192. package/server/providers/index.ts +5 -0
  193. package/server/routes/index.ts +6 -0
  194. package/server/runtime.ts +24 -0
  195. package/server/standalone.ts +29 -0
  196. package/server/tsconfig.json +4 -0
  197. package/tailwind.config.mjs +59 -0
  198. package/tests/components/app-layout-routes.test.tsx +132 -0
  199. package/tests/components/app-title.test.tsx +57 -0
  200. package/tests/components/auth-i18n.test.tsx +87 -0
  201. package/tests/components/back-button.test.tsx +80 -0
  202. package/tests/components/file-preview.test.tsx +101 -0
  203. package/tests/components/header-hover.test.tsx +87 -0
  204. package/tests/components/inbox.test.tsx +139 -0
  205. package/tests/components/language-switcher.test.tsx +298 -0
  206. package/tests/components/layout-containers.test.tsx +15 -0
  207. package/tests/components/layout-i18n.test.tsx +187 -0
  208. package/tests/components/loading.test.tsx +25 -0
  209. package/tests/components/navigation-menu.test.tsx +355 -0
  210. package/tests/components/page-harness.test.tsx +318 -0
  211. package/tests/components/primitive-labels.test.tsx +80 -0
  212. package/tests/components/sidebar-permissions.test.tsx +110 -0
  213. package/tests/components/sidebar-preference.test.tsx +42 -0
  214. package/tests/fixtures/theme-tokens.html +12 -0
  215. package/tests/fixtures/theme-tokens.tsx +111 -0
  216. package/tests/logic/account-permissions.test.tsx +204 -0
  217. package/tests/logic/app-locale-coverage.test.ts +136 -0
  218. package/tests/logic/app-server.test.ts +1589 -0
  219. package/tests/logic/asset-url-build.test.ts +106 -0
  220. package/tests/logic/client-auth.test.tsx +122 -0
  221. package/tests/logic/client-plugin-registry.test.ts +63 -0
  222. package/tests/logic/client-route-recovery.test.tsx +80 -0
  223. package/tests/logic/client-routes.test.ts +126 -0
  224. package/tests/logic/client-runtime.test.ts +112 -0
  225. package/tests/logic/client-settings.test.tsx +841 -0
  226. package/tests/logic/client-shell.test.tsx +466 -0
  227. package/tests/logic/client-theme.test.tsx +441 -0
  228. package/tests/logic/config.test.ts +197 -0
  229. package/tests/logic/dev-client-watch.test.ts +108 -0
  230. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  231. package/tests/logic/device-code-migration.test.ts +70 -0
  232. package/tests/logic/lifecycle.test.ts +221 -0
  233. package/tests/logic/plugin-commands.test.ts +69 -0
  234. package/tests/logic/plugin-page-authz.test.ts +76 -0
  235. package/tests/logic/route-navigation.test.ts +66 -0
  236. package/tests/logic/route-overlay.test.tsx +353 -0
  237. package/tests/logic/server-imports.test.ts +88 -0
  238. package/tests/logic/tailwind-sources.test.ts +151 -0
  239. package/tests/logic/template-package-name.test.ts +88 -0
  240. package/tests/logic/theme-preferences.test.ts +86 -0
  241. package/tests/logic/theme-tokens.test.ts +248 -0
  242. package/tests/logic/toaster.test.tsx +108 -0
  243. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  244. package/tests/setup/client-config.ts +27 -0
  245. package/tsconfig.json +11 -0
  246. package/tsconfig.node.json +12 -0
  247. package/tsconfig.server.json +19 -0
  248. package/vite.config.ts +92 -0
  249. package/vitest.config.ts +37 -0
  250. package/README.md +0 -3
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "shadcn": {
4
+ "command": "pnpm",
5
+ "args": ["exec", "shadcn", "mcp"]
6
+ }
7
+ }
8
+ }
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "shadcn": {
4
+ "command": "pnpm",
5
+ "args": ["exec", "shadcn", "mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,20 @@
1
+ client-old/
2
+ registry/
3
+
4
+ # Generated by the package manager, and reformatting it would produce a file pnpm did not write.
5
+ # A generated application has none of the directories above but does have this one, which is why
6
+ # this file ships in the package: without it `pnpm format:check` fails on a freshly created app.
7
+ pnpm-lock.yaml
8
+
9
+ # Generated by `pnpm nocobase collections generate`: one JSON artifact per Collection plus a `_manifest.json`, written through
10
+ # a stable serializer so that `pnpm nocobase collections generate --check` can regenerate them and compare byte for byte.
11
+ # Prettier collapses their short arrays and objects onto single lines, which makes that check report the artifacts as
12
+ # out of date when nothing about the schema changed.
13
+ database/*/collections/
14
+
15
+ # Synchronized by `pnpm nocobase skills sync` from the installed NocoBase packages: `.agents/skills/` is copied from them and
16
+ # `.claude/skills/` holds symbolic links into it. They are upstream prose, replaced wholesale on the next sync, so
17
+ # formatting them rewrites files this application does not own and the change disappears anyway. `.gitignore` lists
18
+ # both, but a `--ignore-path` run replaces `.gitignore` rather than adding to it, so they are named here as well.
19
+ .agents/
20
+ .claude/skills/
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "shadcn": {
4
+ "command": "pnpm",
5
+ "args": ["exec", "shadcn", "mcp"]
6
+ }
7
+ }
8
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,432 @@
1
+ # Application Development Guidelines
2
+
3
+ This is a NocoBase 3 application. Do not apply globally installed NocoBase 2 Skills. You are building the application itself — its pages, its API, its database tables. Everything under this directory is application-owned source code that you may edit directly.
4
+
5
+ Do not create a plugin to add a feature. Plugins are separately published packages for capabilities shared across several applications; building one for this application's own feature adds a package boundary, a version, and a release process to work that belongs in `client/` and `server/`. Create one only when the user explicitly asks for a reusable published package.
6
+
7
+ When users refer to annotations, marks, or selections in the current application, first run `pnpm exec agent-annotations list --json` from the application root to find Agent Annotations records, ask for clarification only if none match or the target remains unclear, and do not implement annotation requests when the user only asks where they are.
8
+
9
+ ## Default template scope
10
+
11
+ Default is the clean application starting point. It registers product capabilities but no `app-plugin-*-example` plugins, example pages, application sample services, or sample APIs. Keep runnable demonstrations in `app-template-examples`. Application-owned server routes start empty; the only built-in application provider exposes Authorization Permission Sets as direct roles in the Users page. The only application page is a localized homepage.
12
+
13
+ `database/main/` contains required permission initialization only; application-owned business migrations and seeds start empty. Do not add article history, demo seeds, or compatibility copies from Examples to this template. Existing installations retain their own executed migration sources when upgrading; see the [upgrade migration rules](.agents/skills/nocobase-app-upgrade/references/edge-cases.md#migrations).
14
+
15
+ ## Load the development skills
16
+
17
+ `pnpm install` runs `pnpm nocobase skills sync` through the application's `postinstall` hook. If `.agents/skills/nocobase-app-development/` is missing, install dependencies from the application root; if install scripts were disabled or synchronized Skills are stale, run `pnpm nocobase skills sync` explicitly.
18
+
19
+ `.agents/skills/nocobase-app-development/` holds the detailed guidance behind this file. Read its `SKILL.md` first — it routes to the reference that matches your task instead of making you read everything:
20
+
21
+ | Task | Reference |
22
+ | ------------------------------------------------------------ | ------------------------------------ |
23
+ | Any frontend change: pages, routes, components, styles, copy | `references/frontend/ui-workflow.md` |
24
+ | Add an HTTP endpoint | `references/server-routes.md` |
25
+ | Name an endpoint, shape its response or errors, validate it | `references/http-api.md` |
26
+ | Read or write data | `references/database-and-data.md` |
27
+ | Change the schema | `references/migrations.md` |
28
+ | Switch or add a database connection | `references/database-connections.md` |
29
+ | Translate server-produced text or add a language | `references/i18n.md` |
30
+ | Add a service, background job, or scheduled task | `references/services-and-jobs.md` |
31
+ | Write tests and verify | `references/testing.md` |
32
+
33
+ Read the one page your task needs, not the whole directory.
34
+
35
+ `.agents/skills/nocobase-app-upgrade/` is a separate Skill for a separate job: merging a newer release of the template this application was generated from. Read it when the task is upgrading the template rather than building a feature, and read it before touching anything — an upgrade done by copying the newest template over this application destroys the work that made it this application.
36
+
37
+ `.agents/skills/nocobase-deployment/` covers moving this application from source to a production server or a Hub: building for the target platform, what the archive does and does not carry, migrations and business data, production configuration, workflow artifacts after a production build, and what to verify afterwards. Read it before building for deployment, and when an application starts in production but does not work.
38
+
39
+ ## Where things go
40
+
41
+ Business code goes in these places. This is where you work, and where you should stay unless the task genuinely requires otherwise:
42
+
43
+ ```text
44
+ client/routes.ts Declare a page route
45
+ client/pages/ The page component; a folder when a page has children or files of its own
46
+ client/components/ Your components
47
+ client/components/ui/ shadcn/ui primitives; add with the CLI, never hand-write or edit
48
+ client/hooks/ Hooks shared across features
49
+ client/locales/ Every user-visible string
50
+ client/service-provider.ts Client startup: services that boot with the application
51
+ server/routes/ HTTP endpoints
52
+ server/providers/ Services and their lifecycle
53
+ database/main/migrations/ Schema changes
54
+ database/main/seeds/ Required initial data
55
+ database/main/collections/ Generated Collection artifacts; regenerate, never edit
56
+ cli/commands/ Commands this application owns
57
+ tests/ Tests; never beside the source
58
+ ```
59
+
60
+ A page with children or page-local helpers uses a folder with `index.tsx`; child folders mirror route paths. Keep page-local components and data in that folder, reserving `client/components/` for application-wide components. See [child routes](.agents/skills/nocobase-app-development/references/frontend/references/child-routes.md) for examples.
61
+
62
+ A feature with a page and an API touches five places: a migration for the table, a route in `server/routes/`, a page in `client/pages/` declared in `client/routes.ts`, navigation on the page route, and strings in `client/locales/`.
63
+
64
+ `client/runtime.ts` composes the browser application; `client/react-providers.ts` declares React providers in outer-to-inner layers `root`, `application`, and `extension`. Applications use the first two and plugins own the last; `before` and `after` order providers only within their layer.
65
+
66
+ `server/runtime.ts` composes configuration, plugins, providers and routes; `server/app.ts` assembles the application. `server/standalone.ts` starts the Node listener and `server/embedded.ts` lets a host mount the same runtime. Register endpoints through `server/routes/index.ts`; queue consumers and background jobs are registered from a service provider, not discovered from a directory. Editable module defaults live in `server/config/` and are collected by `defaultAppConfigs` in its `index.ts`; `server/config.ts` loads the configuration file. Each section declares the environment variables that set it in `env` of its `defineAppConfig`, with a `description` and, where it applies, `secret`, `required`, `generate` or `firstStartOnly` in the helper's second argument; `pnpm nocobase config env` lists them all, `pnpm nocobase config variables` describes each, and `pnpm build` writes that description to `dist/variables.json` for whoever deploys the build. `server/config/database.ts` maps `DB_*` onto the main connection and `server/config/users.ts` maps `INITIAL_ADMIN_*` onto the initial administrator. `config.yml` is read as written: a `${NAME}` in it is not expanded.
67
+
68
+ ### The rest is framework structure
69
+
70
+ Layouts own breadcrumb route context and render the current page's trail in the header after the sidebar toggle (`client/components/breadcrumbs.tsx`): the route trail by default, or the trail a page declares with `usePageBreadcrumb` from `@nocobase/app-client` when it names records. Pages do not render breadcrumbs themselves. `AppRouter` selects routes and layouts. See [pages and routes](.agents/skills/nocobase-app-development/references/frontend/references/page.md) for each layout's scope.
71
+
72
+ `client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*` and controlled `Collapsible` sub-menus. Leave `client/components/ui/sidebar.tsx` as the shadcn CLI writes it and customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. App and Settings layouts own menus, branding, permissions and redirects. Desktop icon mode shows leaf labels as zero-delay tooltips and opens groups in hover and keyboard-focus popovers, preserving filtered navigation, parent-page links, nested groups and the selected group's highlight. Keep this behavior aligned across layouts. Desktop collapse state is shared through `useSidebarPreference` at `nocobase:sidebar:collapsed` across applications on the same origin; whether the phone sheet is open stays local to each layout.
73
+
74
+ The Settings header entry appears only when the user has an accessible page in the settings navigation, and stays visible on that page. The header reads the registered settings tree through `useClientApplication().runtime.settingsRouteTree`, reusing the application context.
75
+
76
+ `client/routing/`, `client/layouts/`, `client/theme/`, the server entry points, the `package.json` scripts that call `nocobase`, and the tsconfigs are the scaffolding the template provides. It is still this application's own source — it shipped to the user and they may change it — but it is the part the template evolves, so an edit there is what a future upgrade has to reconcile.
77
+
78
+ Prefer the mechanism the system already provides. Declare a page in `client/routes.ts` and add `navigation` when it needs a menu entry. Refine resources add no menu entries and, with no data provider registered, load nothing; pages load data through `useApiClient`. A settings page uses `authz` to restrict access; a plugin page is customized through an option or an override. Before editing the shell to add a menu, check the route and its `navigation` declaration.
79
+
80
+ When the built-in mechanism genuinely cannot express what is being asked, changing this structure is a legitimate answer — not a last resort to apologize for. Do it deliberately, and leave the next agent enough to work with:
81
+
82
+ - Comment what you changed and why the built-in path did not fit. On upgrade, an agent reconciling the template's version needs to know whether your change is still needed.
83
+ - Update this file in the same change, so the guidance describes the application as it actually is. If the framework's shared guidance is wrong for every application, update the source `@nocobase/app-skills` package instead of editing its synchronized copy.
84
+
85
+ ## Building a feature
86
+
87
+ ### Pages and routes
88
+
89
+ Declare the route in `client/routes.ts` and keep the page component behind a lazy `componentLoader`. The loaded module must default-export the component.
90
+
91
+ ```ts
92
+ const appRoutes: AppClientRouteContribution = defineAppRoutes([
93
+ {
94
+ name: 'orders',
95
+ path: '/orders',
96
+ auth: 'required',
97
+ authz: { resource: { type: 'page', id: 'orders' }, action: 'access' },
98
+ componentLoader: () => import('./pages/orders.js'),
99
+ },
100
+ ]);
101
+ ```
102
+
103
+ Route paths are application-internal. Never write the deployment base path such as `/main` into one — the runtime restores it.
104
+
105
+ `auth` controls browser navigation only: `required` for signed-in pages, `guest` for sign-in and registration, `optional` for pages that work either way. It is not server security. An endpoint the page calls enforces its own authentication independently.
106
+
107
+ Use `defineSettingsRoutes()` for administrative pages, which mount under `/settings`, and `defineDevRoutes()` for development-only pages, which mount under `/dev` and are absent from a production build. Dev pages render inside the application shell, like App pages, but have no navigation or header entry: open them by URL. Do not repeat `/settings` or `/dev` in the path. `defineDevRoutes()` is a build boundary, not a permission boundary: a page that must be restricted in production is a settings route with `authz`, enforced by the server.
108
+
109
+ **Declare navigation on the route.** App and Settings menus read `navigation: { title: 'navigation.orders' }`; titles resolve in the owning locale namespace. Add the translation in `client/locales/`. Refine resources do not add menu entries.
110
+
111
+ Use recursive groups to organize menus; their path is optional. Pages may also have children, but must manually render `Outlet`. For URL-addressable dialogs and drawers, declare the child in `defineAppRoutes()` in `client/routes.ts`, place the owning page's `Outlet`, then render `RouteDialog` or `RouteDrawer`. A child page of its own — a record's page, a form too long for a dialog — returns `RouteChildPage` around its `PageContainer`; only tab content renders inline, and a child route that returns a bare `PageContainer` renders below the parent's content instead of covering it. Call `useRouteOverlay()` only from a descendant rendered inside the overlay, including its footer, never from the page returning the wrapper. A record opens over the page the user is on: a dashboard or a board that shows records declares their drawer under its own route instead of linking to the list's, and a row menu's Edit opens its own dialog rather than the one stacked on the drawer. On a page with tabs the user is on the tab being shown: an overlay the page's header opens is declared under every tab and linked through the current tab (`` `${tab}/edit` ``, never a bare `edit`, which resolves beside the tabs), so it stacks on that tab and closing returns to it. Read `.agents/skills/nocobase-app-development/references/frontend/references/overlay.md` before implementing overlays or close guards.
112
+
113
+ Declare `authz` on the first page of every path: `{ resource: { type: 'page', id: 'orders' }, action: 'access' }` for a product page, the page's `settings` item for a settings page, `'unrestricted'` for a page only root may open, or `'skip'`. A nested page that omits it inherits its nearest ancestor page's value, and a child's own value overrides it. A first page that omits it still registers, with a development warning: protected App and settings pages become unrestricted-only, which hides them from everyone but root, while guest, optional and dev pages skip the check. Always declare it rather than relying on that default. Parent guards still apply when a child skips. Menus and loaders read the declared rule; endpoints enforce authorization independently. The page id identifies stored page grants, so changing it requires migrating the grants that reference it.
114
+
115
+ ### Components and styling
116
+
117
+ Use package-local imports such as `#components/ui/button` and `#extensions/nocobase-auth-forms/form-parts`. `package.json#imports` declares their targets, and `components.json` uses the same prefixes for shadcn generation. Keep directory entry points explicitly mapped to their index files. Do not add a Vite alias or TypeScript `paths` mapping for these imports.
118
+
119
+ Use shadcn/ui for UI. The template ships only the primitives its shell and compositions use: `button`, `dialog`, `dropdown-menu`, `input`, `label`, `popover`, `spinner`, `toast` and `tooltip`. Check `client/components/ui/` first; if the primitive is not there, add it from the shadcn registry rather than writing your own. `yes n |` answers "no" when the CLI offers to overwrite a primitive that is already installed; without an answer, a non-interactive run stops at that question. Format the files it created and translate the English a few primitives carry, as `.agents/skills/nocobase-app-development/references/frontend/references/shadcn.md` lists; otherwise keep them as the CLI writes them:
120
+
121
+ ```bash
122
+ yes n | pnpm exec shadcn add card
123
+ pnpm exec prettier --write client/components/ui/card.tsx
124
+ ```
125
+
126
+ Build your own components by composing these primitives, and put them in `client/components/`. A page below another one — a covering child page, a record's own page — leaves by the header's breadcrumb when its route declares `breadcrumb` or the page declares its trail with `usePageBreadcrumb`, and otherwise by `BackButton` (`client/components/back-button.tsx`) above its heading; never both.
127
+
128
+ `PageContainer`, `PageHeader`, `RouteDialog`, `RouteDrawer`, `RouteChildPage` and `useRouteOverlay` in `client/components/` come from the NocoBase UI Library, as do the authentication blocks in `client/extensions/nocobase-auth-forms/`, `nocobase-auth-methods/`, `nocobase-auth-split-layout/` and `nocobase-device-approval/` that `client/pages/auth/` composes, the inbox block in `client/extensions/nocobase-inbox/` that `client/pages/inbox.tsx` renders on `/inbox`, and `InboxButton` with its `inbox-badge.ts`, which `client/components/inbox-header-button.tsx` places in the header with the in-app notification plugin's unread count; like the rest of the source, they belong to the application. `yes n | pnpm exec shadcn add @nocobase/<item>` adds another item from the library: a single component lands in `client/components/`, a complete feature in `client/extensions/nocobase-<item>/`. A date field and a list's `DataTable` are the application's own components: compose `DatePicker` in `client/components/date-picker.tsx` from the `calendar` and `popover` primitives, following shadcn's Date Picker guide, and build `DataTable` in `client/components/` from the `table` primitive and `@tanstack/react-table`, following shadcn's Data Table guide. The CLI adds the packages an item or primitive needs to `dependencies`: move the new ones, such as `react-day-picker` for `calendar`, to `devDependencies`, as "Adding a dependency" below explains for everything client code imports, but leave `@nocobase/i18n` in `dependencies`, where the server needs it, and put back the `^` of its range, which the CLI pins.
129
+
130
+ **Look components up in the shadcn skill.** The development Skill carries the shadcn/ui skill, unchanged from shadcn 4.21.0, in `.agents/skills/nocobase-app-development/references/frontend/shadcn/`. Read `references/frontend/references/shadcn.md` first: it routes to the skill's rules and states where this application departs from them, among others `pnpm exec shadcn` instead of `npx shadcn@latest`, no `shadcn apply` or `--preset`, and create, edit and detail as `RouteDialog` or `RouteDrawer` child routes rather than a `Sheet`. `pnpm exec shadcn docs <name>` gives one primitive's documentation and examples for the Base UI version. For a whole page, start from the worked example that `references/frontend/references/example.md` indexes.
131
+
132
+ Style with the semantic Tailwind tokens — `bg-background`, `text-foreground`, `text-muted-foreground`, `border-border`, `bg-primary` — so pages follow the light and dark themes. Do not hard-code colors like `bg-white` or `text-gray-900`; they break the moment someone switches theme.
133
+
134
+ **Visual consistency is a whole-application property.** Match the surrounding code's spacing, typography, and component choices. If a change genuinely calls for a different look, change the application's design tokens in `client/theme/themes/*.css` so every page moves together. Never restyle only the part you are working on — a page that looks different from the rest is a defect, not a customization.
135
+
136
+ **Follow the UI guidelines.** `.agents/skills/nocobase-app-development/references/frontend/ui-guidelines.md` sets the conventions every page follows, among them the page header and the single placement of the primary action, empty states, row actions behind one "…" menu, dates picked from a `Calendar` in a `Popover` rather than a native date input, and medium-width drawers; the frontend workflow says how much of it to read for a change.
137
+
138
+ ### Server routes
139
+
140
+ `defineApiRoutes()` mounts under `/api`, and `defineRootRoutes()` mounts at the root for callbacks and webhooks. Do not repeat `/api` in the path. Each route factory creates and returns its own Hono router.
141
+
142
+ ```ts
143
+ export const apiRoutes: AppApiRouteContribution<Application> = defineApiRoutes(
144
+ (app) => {
145
+ const router = new Hono();
146
+ const auth = app.container.resolve(authenticationToken);
147
+
148
+ router.use('/orders', auth.required());
149
+ router.get(
150
+ '/orders',
151
+ describeRoute({
152
+ tags: ['Orders'],
153
+ summary: 'List orders',
154
+ operationId: 'listOrders',
155
+ responses: {
156
+ '200': listResponse(OrderSchema),
157
+ // A session but no permission check; the query validator adds the 400.
158
+ '401': apiErrorResponse(401),
159
+ '500': apiErrorResponse(500),
160
+ },
161
+ }),
162
+ apiValidator('query', ListOrdersQuery),
163
+ async (context) =>
164
+ context.json(await listOrders(context.req.valid('query'))),
165
+ );
166
+
167
+ return router;
168
+ },
169
+ );
170
+ ```
171
+
172
+ **Every `/api` route follows the HTTP API design** in `.agents/skills/nocobase-app-development/references/http-api.md`: camelCase paths, `{ data }` on success, `throw new ApiError(...)` from `@nocobase/app-server/router` on failure, input validated with zod through `apiValidator()`, and a `describeRoute()` declaration for the API document, all from `@nocobase/app-server/router`. Never write an error body by hand.
173
+
174
+ **Every route owns its own security.** Mounting under `/api` does not authenticate anything. Install `auth.required()` on the paths the route owns, and add `authorization.middleware()` with an explicit `resource`/`action` check when the operation needs permission rather than just identity. Never rely on middleware from another route or on the order routes happen to be registered in.
175
+
176
+ Scope middleware to the exact paths you own, or to an isolated sub-router mounted at your prefix. A `router.use('*', ...)` leaks into contributions mounted later.
177
+
178
+ A webhook that a third party calls cannot use a login session, so it is deliberately public — but public still means verifying a signature, timestamp, or one-time state, and testing that anonymous requests without a valid signature are rejected.
179
+
180
+ Keep HTTP concerns in the route and domain logic in a service under `server/providers/`. Services do not read Hono contexts, return HTTP status codes, or decide retry behavior.
181
+
182
+ Bind services to their existing tokens in a provider's `register()`; calling `createServiceToken` twice with the same name creates different keys. Do not connect to databases, start workers, or execute route factories at module top level. Acquire long-lived resources in `start()` and release them in `shutdown()`. Providers and routes read typed configuration rather than `process.env`.
183
+
184
+ ### API documentation
185
+
186
+ The running application serves an OpenAPI 3.1 document of every `/api` route it has — its own, every registered plugin's, the data endpoints and Better Auth's `/api/auth/...` — at `<origin><APP_BASE_PATH>/api/swagger` (JSON), with Swagger UI at `<origin><APP_BASE_PATH>/api/swagger/docs`. Locally that is `http://127.0.0.1:13000/main/api/swagger/docs`, with `/main` replaced by the actual `APP_BASE_PATH`.
187
+
188
+ Reading either needs a signed-in session or an API key: open the Swagger UI in a browser where you are signed in, or send a key in `x-api-key`. A user creates keys at `<APP_BASE_PATH>/settings/api-keys`; the `nocobase-app-plugin-api-keys` Skill covers the plugin. Without a valid credential the routes answer `401` with reason `API_DOCS_UNAUTHENTICATED`, and an application with no access check registered, such as one without the authentication plugin, answers `404`.
189
+
190
+ ```bash
191
+ curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
192
+ ```
193
+
194
+ **Learn the available endpoints from this document instead of reading route sources.** Before calling an endpoint or building on one, fetch the JSON with a key the user gives you and find the operations by `tags` or `operationId`; read the source only for what the document does not say. Fetch it again after registering a plugin or changing a Collection.
195
+
196
+ Every `/api` route the application writes declares itself, as in the example above: `tags`, an English `summary` and a unique camelCase `operationId`; input through `apiValidator()`; responses through `dataResponse()`, `listResponse()`, `emptyResponse()` and `apiErrorResponse(status)` for exactly the statuses the route can produce. Do not list `400` for input validation: a route that uses `apiValidator` gets the `400` automatically. List `400` yourself only for another reason, such as a failed precondition. `apiErrorResponses` is `401`, `403` and `500`, for an authenticated route with a permission check; otherwise list each status the route can return with `apiErrorResponse(code)`. Response schemas live in `server/routes/schemas.ts`, typed against what the service returns. A route reached without a credential adds `security: []`. Hide a route with `describeRoute({ hide: true })` and a comment only for the few reasons `references/http-api.md` lists. Data endpoints are documented without a declaration; a field a data exposure adds to every record goes in its `computedFields`. A test starting the application expects `findUndeclaredApiRoutes()` and `findApiDocumentSchemaProblems()` to be empty; `.agents/skills/nocobase-app-development/references/testing.md` shows it.
197
+
198
+ ### Database
199
+
200
+ Schema changes are migrations under `database/main/migrations/`. Data the application requires to run is a seed under `database/main/seeds/`. Seeds never create structure. Sample data is not required data: it is a seed declared with `defineSeed({ name, sample: true, run })` in the same directory, or, when it has to go through plugins' services, a registration on `sampleDataToken` from `@nocobase/app-server/sample-data`. Either loads only when the database is installed with `app.sampleData` set (`APP_SAMPLE_DATA=true`) and is recorded as skipped otherwise; `pnpm nocobase db sample` loads what was skipped, in development.
201
+
202
+ Declare database defaults with `export default defineAppDatabaseConfig((runtime) => ({ connections }))`. Before provider registration, the runtime asynchronously imports configured official drivers; explicit `drivers` registrations override them. Keep `isolatedDeclarations: false` for application server declarations so configuration and connection fields retain inference. See `.agents/skills/nocobase-app-development/references/database-connections.md`.
203
+
204
+ `database/<connection>/collections/` holds what the database currently resolves each Collection to — `collection.json`, `metadata.json` and `schema.json` per Collection plus a `_manifest.json` — written by `pnpm nocobase collections generate`, which `db apply`, `db redo`, `db rollback`, `db reset` and `pnpm dev` run for every connection whose migrations they change. Every file there is derived, for every connection: the directory is gitignored, safe to delete, and never read back. Edit metadata through migrations or the Collection Metadata Service and regenerate, never by hand, and never import these files from a migration. An `external` connection, if you add one, takes its hand-written metadata from `database/<connection>/metadata/<name>.json` instead, which is committed. `pnpm nocobase collections generate --check` fails when they are out of date.
205
+
206
+ ```ts
207
+ const migration: MigrationDefinition = defineMigration({
208
+ name: '202609020001_create_orders',
209
+ async up({ builder }) {
210
+ await builder.createCollection('orders', (collection) => {
211
+ collection.increments('id');
212
+ collection.string('reference', { length: 64, nullable: false });
213
+ collection.datetime('createdAt', { nullable: false });
214
+ });
215
+ },
216
+ async down({ builder }) {
217
+ await builder.dropCollection('orders');
218
+ },
219
+ });
220
+ ```
221
+
222
+ **A migration is immutable history and must be self-contained.** Spell out every field, index, and constraint in the migration itself. Never import a collection definition, model, or registry that keeps evolving — doing so silently changes what an already-applied migration means. Write `down` as the explicit reverse in a safe dependency order.
223
+
224
+ Edit an existing migration only while the branch that introduced it is unmerged. Once merged, every correction is a new migration. Editing one that has already run changes nothing on its own: it is recorded as executed, so `pnpm nocobase db apply` skips it. Run `pnpm nocobase db redo` to roll the latest batch back and apply it again from the corrected source. Editing it also makes its recorded checksum stop matching; a run reports that as a warning and keeps going, `onChecksumMismatch: 'error'` makes it refuse, and `pnpm nocobase db repair` realigns the history for a change that leaves the schema identical — a reformat or a comment — never for one the database has not received. `pnpm nocobase db reset` starts over from an empty schema when the batch cannot be rolled back, and takes every row with it.
225
+
226
+ The exported `name` must match the filename. Apply with `pnpm nocobase db apply` and verify against a real database.
227
+
228
+ At runtime, resolve `databaseManagerToken` from the container and use `database.query()` to read and write the default connection. Use `database.query('analytics')` for another connection. Application tasks use `database/<connectionName>/{migrations,seeds}` and bind to that connection explicitly; plugin tasks and default runtime access stay on `database.default`. Only managed connections run migrations or seeds. See the migrations reference for execution and upgrade rules.
229
+
230
+ ### User-facing text
231
+
232
+ Every string a user reads goes through a translation key. `client/locales/en-US.ts` states the wording and derives the shape that other locales are checked against, so a missing key in `zh-CN.ts` is a compile error. The sign-in pages' copy lives there too, under `auth.*`: the authentication components take their text as props, which the pages in `client/pages/auth/` translate.
233
+
234
+ ```tsx
235
+ const { t } = useTranslation();
236
+ t('orders.title');
237
+ ```
238
+
239
+ To reword a plugin's string, add an `overrides` block keyed by that plugin's package name in your locale file. Do not edit the plugin.
240
+
241
+ The languages the application offers are its own locale files, not a configured list, and the two sides are read separately: `client/locales/` decides what the picker shows, while `server/locales/` decides which languages the server can answer in. Prefer adding a language to both when server-produced text needs translating, but a client-only language is valid: the interface switches normally and the server falls back to English with an informational notice. `pnpm nocobase locales check` reports one declared on a single side and exits nonzero until the lists align; that check does not block the runtime switch.
242
+
243
+ The account menu language control in `client/layouts/components/language-switcher.tsx` uses a shadcn submenu with radio items. Render it inside `DropdownMenuContent` to preserve menu keyboard navigation and selection semantics.
244
+
245
+ ## Development file watching
246
+
247
+ Changes to `.env` and `.env.local` (including creation, atomic replacement, and deletion) restart the full development run after its previous processes exit. The new run reloads environment files, ports, proxy settings, and startup hooks; explicit shell variables still take precedence. Use the newly printed URL if the port or base path changes. This also applies in proxy mode. `config.yml` changes restart only the local server. `NOCOBASE_STRICT_STARTUP=true` disables both automatic restarts.
248
+
249
+ Changes to `package.json`, the lockfile, and the package manager's install state restart the local server once they have been quiet for a few seconds. An install writes several of them over as long as fetching and linking take, so restarting on the first write would bring the server back against a half-installed `node_modules` — and the next write would arrive while it was still shutting down, which is where the watcher escalates to SIGKILL. Waiting for quiet turns one install into one restart.
250
+
251
+ One development server runs per application root; a second is refused with the first one's process id. Nothing else catches it, because the port check advances to the next free port and the duplicate then fails on the migration lock the first server holds, long before it binds anything. `NOCOBASE_DEV_ALLOW_MULTIPLE=true` starts one anyway. A run that only proxies a remote backend does not take the lock, and a lock left behind by a killed run is taken over rather than reported.
252
+
253
+ `pnpm dev` also shortens the shutdown budget, through `APP_SHUTDOWN_TIMEOUT_MS`, to less than the five seconds the file watcher waits before force-killing the server. The deployment defaults — a 30 second HTTP drain behind a load balancer — would never be reached here, and a force-killed server never releases its migration lock. Set the variable explicitly to override it in either direction.
254
+
255
+ Tests that start auxiliary Vite servers must use an isolated temporary `cacheDir`, including when their fixture links the application's `node_modules`. Never delete or rewrite a running development server's dependency cache. See the shared application development Skill's `references/testing.md` for cache ownership and recovery.
256
+
257
+ `pnpm dev` checks native file watching before starting its children. If watcher resources are exhausted or native events are unavailable, it uses polling for client and server hot updates and disables agent annotations for that run, with a warning. An explicit `CHOKIDAR_USEPOLLING=true` selects the same mode. Configuration files use stat polling so atomic saves and newly created files restart the server without native directory watchers.
258
+
259
+ Vite must exclude the application's entire `dist/` tree from development file watching. Its default exclusion covers only `dist/client`; watching the compiled server and vendored packages can cause `EMFILE` after a build. Keep the exclusion scoped to this application so linked workspace dependencies, including their `dist/` files, still receive hot updates.
260
+
261
+ ## Developing against another backend
262
+
263
+ Do not hard-code `/main` from the examples below. It is only the fallback for an unset `APP_BASE_PATH`. Local development resolves that variable from the command-line environment, then `.env.local`, then `.env`. The remote mount path is independently supplied in `PROXY_TARGET_URL`: inspect the target application's actual public URL instead of assuming it matches the local path. For example, `APP_BASE_PATH=/local PROXY_TARGET_URL=http://127.0.0.1:13000/crm pnpm dev` forwards local `/local/api` and `/local/ws` to remote `/crm/api` and `/crm/ws`.
264
+
265
+ Use `PROXY_TARGET_URL=https://backend.example.com/main pnpm dev` to run only the local Vite client against another application's API and WebSocket service. The URL is the remote application base, including its mount path, without `/api`. Open the printed Local URL. The local server and server watchers are skipped; `beforeDev` hooks still run. Requests, including writes, affect the target backend. See README.MD for path mapping and authentication requirements. Leave the variable unset for normal full-stack development.
266
+
267
+ The development proxy adapts same-origin HTTP and WebSocket Origin headers to the target origin, and maps same-origin Referer paths to the target app base. Other origins remain unchanged and missing Origin headers are not added. Keep backend origin checks enabled; this adaptation belongs only to Vite development, not production deployment. Production uses `APP_PUBLIC_ORIGIN` and a reverse proxy that preserves the public Host and protocol.
268
+
269
+ `APP_SERVER_PORT` selects the local application entry port: Vite in proxy development mode (default 5173), or the local backend in normal development mode (default 13000). Normal development keeps Vite's preferred port at 5173. An occupied port advances to the next available port; use the printed Local URL. This variable does not change the backend URL supplied through `PROXY_TARGET_URL`.
270
+
271
+ ## The command line
272
+
273
+ `pnpm nocobase <topic> <command>` runs this application's CLI, the `nocobase` bin of `@nocobase/app-cli`. Only `pnpm dev`, `pnpm build`, `pnpm start` and the quality scripts are scripts; every other command goes through `pnpm nocobase`, with no script alias. `pnpm nocobase --help` lists the tree and `--help` on a command gives its flags.
274
+
275
+ Before running a command, read `.agents/skills/nocobase-app-development/references/cli.md`: it maps tasks to commands, explains the data-model snapshot under `database/<connection>/collections/`, and states the flags an agent must not add on its own — `db reset`, `db rollback`, `db redo` and `db repair` refuse to run without `--force` outside a terminal, and that refusal is not something to work around.
276
+
277
+ A command this application owns is a file under `cli/commands/` whose path is its name below the `app` topic: `cli/commands/sync-orders.ts` answers to `pnpm nocobase app sync-orders`. Each extends `AppCommand` from `@nocobase/app-cli`: it returns its result, throws `CommandError` on failure, and gets `--json` for free. A command that needs the application creates it with `this.withApp(async ({ app }) => …)`, which always shuts it down again; work users trigger while the application serves is a server route or a job, not a command. The `nocobase-app-development` Skill's reference on adding an application command covers the file layout, the directory names to avoid, the output contract and testing.
278
+
279
+ `cli/` is compiled into `dist`, and a deployment runs the same CLI without the development commands as `node dist/cli/index.js <topic> <command>`. Change shared behavior in `@nocobase/app-cli` rather than copying it into the application; use plugin build and dev hooks for local build and development extensions.
280
+
281
+ ## Plugins
282
+
283
+ Plugins are registered in `client/plugins.ts`, `server/plugins.ts`, and `cli/plugins.ts`. Presence in the array enables a plugin and array order is contribution order. A plugin appears in the roots matching what it ships, so a plugin with only commands is listed in `cli/plugins.ts` alone. Bulk Skills synchronization and plugin updates discover plugins from these composition roots.
284
+
285
+ Let `pnpm nocobase plugin register` and `pnpm nocobase plugin unregister` add and remove entries. Edit these files by hand only to reorder entries or to pass a plugin its options.
286
+
287
+ Update one registered plugin with `pnpm nocobase plugin update @nocobase/app-plugin-authentication`, or omit the name to update all registered plugins. Prefer the full package name; `authentication` is also accepted as a short name. The name is a positional argument, not `--plugin`. Use `--dry-run` to preview. With pnpm, updates stay within declared version ranges; after a successful update, all registered plugin Skills are re-synchronized. See [Plugins in the README](README.MD#plugins) for examples and update scope.
288
+
289
+ To customize a page a plugin owns, pass an option on its registration, add a source extension under `client/extensions/*/extension.ts`, or add an entry to `client/route-overrides.ts`. Do not redeclare the plugin's route — a duplicate `/install` is a conflict, not a customization. An override replaces only `componentLoader`; route identity, path, and auth mode stay with the plugin. One route takes one override across all three mechanisms. Authentication pages are not plugin-owned: `/login`, `/register`, `/forgot-password`, and `/reset-password` are application routes declared in `client/routes.ts`.
290
+
291
+ Route overrides must stay lazy, declare a `componentEntry`, and load a default-exported component. Authentication pages in `client/pages/auth/` use relative links and the authentication plugin's `client/actions` hooks; do not call authentication endpoints directly or create a second session store.
292
+
293
+ ### Read a plugin's Skill before building what it already does
294
+
295
+ **You are not starting from scratch.** This application ships with NocoBase packages that already solve whole categories of requirement, and packages may publish a Skill explaining how to use them. `pnpm nocobase skills sync` copies Skills from direct `@nocobase/*` dependencies and registered plugins into `.agents/skills/`. Before implementing a feature, check whether an installed and registered plugin already covers it:
296
+
297
+ | The requirement sounds like | Read the Skill for |
298
+ | ------------------------------------------------------------------------------------------------- | ------------------------------------- |
299
+ | Approvals, multi-step processes, "when X happens then Y", business rules that outlive one request | `@nocobase/app-plugin-workflow` |
300
+ | Email, IM, or in-app messages; notifying someone that something happened | `@nocobase/app-plugin-notification` |
301
+ | Roles, permissions, "user A may only see their own records", field-level or row-level access | `@nocobase/app-plugin-authorization` |
302
+ | Sign-in, registration, sessions, password reset | `@nocobase/app-plugin-authentication` |
303
+ | User listing, account state, password reset, and application-owned role assignment | `@nocobase/app-plugin-users` |
304
+ | File upload and metadata through Repository | `@nocobase/app-plugin-file` |
305
+ | Translated text and language switching | `@nocobase/app-plugin-i18n` |
306
+
307
+ Run `pnpm nocobase skills sync` if `.agents/skills/` is missing or looks out of date, then read the Skill for the plugin you need. It documents that plugin's public entries, the ownership boundary, and how to verify the result — which is faster and more correct than inferring an API from its source.
308
+
309
+ Building a permission system, a notification sender, or a job scheduler by hand when a registered plugin already provides one is the most expensive mistake available here. Prefer the plugin; write your own only when you have read its Skill and confirmed it genuinely does not fit.
310
+
311
+ `.agents/skills/` is generated output: gitignored, and every synchronized package-owned directory is replaced wholesale on the next sync, so never edit a file there. The same sync mirrors each synchronized directory into `.claude/skills/` as a symbolic link, because Claude Code discovers skills only there; that mirror is generated and gitignored too. Put application-specific guidance in committed `AGENTS.md` files.
312
+
313
+ ## Removing a NocoBase dependency
314
+
315
+ Before removing a direct `@nocobase/*` dependency, search application imports, Client/Server/CLI plugin registrations, routes, services, configuration, tests, and build scripts for its package name and public contracts. Migrate or delete those references first. The command below changes dependency metadata and generated Skills; it does not edit application source or configuration, so never use it to remove a capability the application still needs.
316
+
317
+ Use `pnpm nocobase package remove @nocobase/example`. It invokes the application's package manager so `package.json` and the lockfile stay consistent, then deletes only synchronized Skills recorded as belonging to that package. For an `@nocobase/app-plugin-*` package it delegates to the plugin unregister workflow, removing its Client, Server, and CLI registrations together; `pnpm nocobase plugin unregister <name>` remains a supported plugin-specific entry point. Preview with `--dry-run`, and use `--json` when another tool needs structured output.
318
+
319
+ After an interrupted or manual removal, first confirm the manifest no longer declares the package, then run a full `pnpm nocobase skills sync` to reconcile stale package-owned output. `package remove` may also be passed a package already absent from the manifest to clean recorded historical Skill ownership, and it must not uninstall or clean Skills owned by another package.
320
+
321
+ ## Adding a dependency
322
+
323
+ Keep `@nocobase/db` in `dependencies` alongside the database driver. It supplies the driver's runtime peer and lets TypeScript resolve the inferred database configuration declaration through the public package name. Moving it to `devDependencies` can cause TS2883 in an installed application even while the source workspace builds successfully.
324
+
325
+ Put a package your **server** code imports in `dependencies`. Put everything your **client** code imports — along with build tooling, tests, and type-only imports — in `devDependencies`.
326
+
327
+ That split looks backwards until you see how the two halves are deployed. `pnpm build` bundles the client: Vite resolves every client import and inlines it into `dist/client`, so nothing has to resolve it again later. The server is not bundled. `dist/server` keeps its bare imports, and `pnpm build` generates `dist/package.json` from your `dependencies` and installs a `node_modules` next to it — that tree is what the deployed server resolves against, and `devDependencies` are not in it.
328
+
329
+ So the two mistakes fail in opposite ways. A server import left in `devDependencies` works all through development and fails only on the server, with a bare `Cannot find package` naming nothing that points back here. A client package put in `dependencies` never breaks anything — it is just installed into every deployment, where the server never requires it. That one is invisible, so it accumulates: `lucide-react` and `@xyflow/react` were 44 MB of it before this rule was written down.
330
+
331
+ What decides it is where the importing file lives and what the import is, not what the package is for. `import ts from 'typescript'` in `server/` is a runtime dependency even though TypeScript sounds like tooling. `import type { Config } from 'x'` is erased before anything runs, so it stays a devDependency wherever it appears. A dynamic `import()` counts — deferring the load changes when a package is needed, not whether.
332
+
333
+ ### Adding a dependency
334
+
335
+ Which half of the application imports it decides where it goes.
336
+
337
+ | The import is reached from | Declare it in |
338
+ | --------------------------------------- | ----------------- |
339
+ | `server/`, `database/`, or `cli/` | `dependencies` |
340
+ | `client/`, build tooling, tests | `devDependencies` |
341
+ | `import type` only, wherever it appears | `devDependencies` |
342
+
343
+ `dist/package.json` is generated from `dependencies` and is what a deployment installs from, so a server import declared as a devDependency resolves in every development checkout and is absent exactly once — on the deployed server. A client import needs nothing at runtime: Vite resolves and inlines it into `dist/client` at build time.
344
+
345
+ A plugin's browser packages arrive by a third route and need nothing from you. Plugins declare those as peer dependencies, so installing a plugin brings one shared copy into this application, while `dist/` sets `autoInstallPeers: false` and installs none of them.
346
+
347
+ `pnpm build` checks the server half: it reads every value import in `server/`, `database/`, and `cli/` and fails the build if any of those packages is missing from `dist/package.json`. It cannot see an import whose specifier is built at run time:
348
+
349
+ ```ts
350
+ await import(`${name}/index.js`); // invisible to the check
351
+ ```
352
+
353
+ Declare such a package in `dependencies` when you write the code; nothing will remind you later.
354
+
355
+ ### CDN assets at build time
356
+
357
+ `CDN_BASE_URL` sets Vite's `base` only during a production build: `CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build`. Use a full HTTPS URL ending in `/`; unset, empty or whitespace-only values keep the default `./`. `pnpm build` reads it from the process environment, then `.env.local`, then `.env`; development ignores it and continues using `APP_BASE_PATH`. It is a build parameter, absent from `config variables` and `dist/variables.json`, with no `config.yml` setting. Changing it requires rebuilding.
358
+
359
+ Upload the static files from `dist/client/` to that CDN prefix with their directory structure preserved, and enable CORS for the application's origin. Keep `index.html` and the complete client tree on the application server so it can inject runtime configuration into HTML and serve files still addressed there. Use `resolveAssetUrl('/assets/logo.png')` from `@nocobase/app-client` for a file shipped as `dist/client/assets/logo.png`; template logos use this helper. It reads Vite's build-time `BASE_URL`, uses an absolute HTTP(S) CDN base when configured, and otherwise falls back to the runtime mount path. Keep API paths, page links and runtime-generated files such as workflow artifacts on `resolveAppUrl`. Vite does not rewrite arbitrary URL strings. The README's CDN section describes deployment and Docker usage.
360
+
361
+ For a Docker source build, pass `--build-arg CDN_BASE_URL=https://cdn.example.com/my-app/v1/`. With `DIST=prebuilt`, set `CDN_BASE_URL` when building `dist/` beforehand. Neither a prebuilt image's build argument nor `docker run -e CDN_BASE_URL=...` can change compiled asset URLs.
362
+
363
+ ### Packing the build for a deployment
364
+
365
+ `pnpm build --tar` writes `storage/exports/dist.tar.gz` after the build. The archive holds `dist/` as a directory next to `config.example.yml`, so extracting it produces exactly those two paths rather than scattering `server/` and `node_modules/` into whatever directory you unpacked in.
366
+
367
+ `config.example.yml` travels with it because a deployment has to write a `config.yml` before it can start — `pnpm nocobase config init`, run inside `dist/`, does that from the example, in place, and `pnpm nocobase config check` there verifies it, database connection included, before the first start — and the example is the only statement of what may go in it. Directories of executable shims are left out: a `.bin` entry points at a path on the machine that installed it, and a dangling one makes `pnpm install` in the extracted tree report a corrupt store rather than repair it.
368
+
369
+ Without `--tar` no archive is produced, which is what you want when the build is only going to be run locally.
370
+
371
+ ### Building a Docker image
372
+
373
+ `Dockerfile` and `Dockerfile.dockerignore` build this application from its sources: a build stage runs `pnpm install --frozen-lockfile` and `pnpm build`, and a `node:24-bookworm-slim` runtime stage receives `dist/` and `config.example.yml` only. The runtime has no pnpm, so it runs the two `dist/package.json` scripts directly: `start` is `node ./server/standalone.js`, which the image starts as `node dist/server/standalone.js`, and `nocobase` is `node ./cli/index.js`, run as `node dist/cli/index.js <topic> <command>`. The deployment root is `/app`, so configuration is `/app/config.yml` and storage is `/app/storage`, both mounted at runtime.
374
+
375
+ The client is not tied to a mount path, so `APP_BASE_PATH` is a runtime variable, `/main` in the image unless the container is started with `-e APP_BASE_PATH=<path>`; the health check probes the same path. The build stage runs on `$BUILDPLATFORM` and passes `--target linux-$TARGETARCH`, so a multi-platform build does not compile under emulation; do not move the build stage to the target platform or switch the runtime to Alpine, which would need the musl target. Keep `Dockerfile.dockerignore` beside the Dockerfile: BuildKit reads it only there, and without it `config.yml`, `.env`, and `storage/` enter the build context.
376
+
377
+ `--build-arg DIST=prebuilt` skips the build stage and packages the `dist/` in the context instead, selected through `FROM dist-${DIST}` so BuildKit never runs the stage it does not use. That stage refuses a `dist/` whose `nocobase.buildTarget` is not linux, glibc, the target architecture and the image's Node major, and one from an `@nocobase/app-cli` that does not record `nocobase.relocatable`, whose client is tied to the path it was built for. The ignore file re-includes `dist/` last and whole, so dependency directories inside it named like excluded paths (`langchain/storage`) survive, and then excludes `dist/.env`, which can carry `DB_PASSWORD` from local `.env` files. Keep those rules in that order.
378
+
379
+ ### Building for another platform
380
+
381
+ `pnpm build --help` (or `-h`) lists build options and exits without loading build dependencies, running hooks, or modifying `dist/`. Every successful build records `nocobase.buildTarget` in `dist/package.json`, including builds with no native modules: `platform`, `arch`, `libc`, `nodeMajor`, and `nodeAbi`. Use `libc` only for Linux; its value on other platforms is a compatibility placeholder. Deployment checks should compare these fields with the host runtime and also respect `engines.node`. With `--target current` (the default), the Node version and ABI come from the running process; an explicit platform target defaults to Node 24 unless `--node-version` is supplied.
382
+
383
+ `pnpm build` targets the machine it runs on, so `pnpm build && pnpm start` works. A deployment build says where it is going: `--target linux-x64`, `--target linux-arm64`, `--target linux-x64-musl`, plus `--node-version` when the server's Node major differs. Every build prints the platform it produced and records it in `dist/package.json` under `nocobase.buildTarget`.
384
+
385
+ A `.node` binary must match the target platform, architecture, and C library; addons using the Node ABI must also match its version. `better-sqlite3` 13 uses N-API and bundles its platform binaries, so cross-platform builds retain the target's binary, including the separate `linuxmusl` build for Alpine. `pg`, `mysql2`, and `tedious` are plain JavaScript, so an application using only those is portable as built.
386
+
387
+ ### When a build or a deployment fails
388
+
389
+ | Symptom | Cause | Fix |
390
+ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
391
+ | `Cannot find module 'x'` on the server, works locally | `x` is in `devDependencies`, which `dist/package.json` is not generated from | Move it to `dependencies` |
392
+ | A browser package will not resolve while building the application | A plugin declares it as a peer and nothing provides it | Add it to the application's `devDependencies` |
393
+ | `Error loading shared library`, `invalid ELF header`, or a bare `.node` path at startup | The binary does not match the server | Compare `require('./dist/package.json').nocobase.buildTarget` with the server's `process.platform`, `process.arch`, and `process.versions.modules`, then rebuild with matching flags |
394
+ | `no prebuilt binary for <target>` during the build | The package publishes no build for that combination | Check the package supports the target; musl coverage is thinner than glibc |
395
+
396
+ Node ABI to major version: 115 is Node 20, 127 is 22, 137 is 24, 147 is 26.
397
+
398
+ ## Before you finish
399
+
400
+ For each change, scope all verification to the affected files, projects, or packages and their affected consumers. This applies to formatting, lint, type checking, tests, builds, and runtime verification. Run only the checks relevant to the change: use explicit file paths for formatting, lint, and tests; use the owning project's TypeScript configuration for type checking; build only affected packages or supported build targets. In a workspace, use `pnpm --filter <affected-package> <script>`. In a standalone application, use its supported file or project selectors; do not invent flags or bypass project configuration to force a narrower check.
401
+
402
+ Do not run full-application or workspace-wide checks, or an aggregate `pnpm check`, as a routine step after each edit. If a necessary check cannot be narrowed further, run the smallest supported project or package scope and explain why. Expand scope only when shared code, dependencies, configuration, or a failure gives a concrete reason, or when the user explicitly requests it. After checks pass, repeat them only for further relevant changes or unresolved failures. Documentation-only changes need formatting and link checks for the changed documents, not type checking, runtime tests, or builds.
403
+
404
+ Report which checks ran, their scope, and any unverified behavior. See the application development Skill's `references/testing.md` for selection examples.
405
+
406
+ Add tests for what you changed: a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior. Tests belong in `tests/`; browser tests go in `tests/playwright/`, which Playwright runs against a running application at `APP_URL` and Vitest skips. Never place a test beside the source it covers.
407
+
408
+ For any UI work, start with `.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md` (from the application root), which decides the workflow and routes to the rest. Its styling rules are in `references/styling.md` beside it: which semantic Tailwind utilities to use so components respond to theme changes, and how to keep deliberate fixed-size exceptions explicit. The full color, font, size, spacing, radius and shadow token contract, and creating or editing theme presets, are in `references/theme.md`.
409
+
410
+ Application startup defaults belong in `config.yml`: `i18n.defaultLocale` for the language, and `client.app.defaultColorScheme` and `client.app.defaultTheme` for appearance. Valid browser-local choices take precedence. Which languages the application offers is not configured — its own `client/locales/` and `server/locales/` are that list. See the i18n and themes references.
411
+
412
+ ## Compiled migration and seed manifests
413
+
414
+ The application build generates `.manifest.json` in each compiled migrations and seeds directory after server compilation, path rewriting, and `afterServerBuild` hooks. Keep the manifest generator in the build when customizing it. Plugins generate their own manifests when built; an application must not regenerate manifests for installed dependencies.
415
+
416
+ TypeScript and compiled JavaScript use the same source checksum for migration history, while the loader separately verifies emitted JavaScript. Marked JavaScript requires its manifest. For a database with old raw JavaScript checksums, first run the compiled representation with matching original output; verified legacy hashes are converted under the task lock. Unreproducible old output remains an error. Never edit historical migrations, and never edit the history table by hand, to resolve an upgrade failure; `pnpm nocobase db repair` is the supported way to realign a checksum you can account for, and `pnpm nocobase db redo` the way to re-run a migration whose branch is still unmerged.
417
+
418
+ The account menu checks Better Auth sign-out results before refreshing the session and shows a localized error toast for API or network failures. Preserve this behavior when upgrading the shell; navigation alone does not revoke a session.
419
+
420
+ The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Pending checks hide protected content, and failed checks deny access.
421
+
422
+ Navigation groups retain their expanded or collapsed state while the navigation tree stays mounted. Selecting a new page expands its ancestor groups without collapsing other groups; users can still collapse the active group manually. Keep this behavior aligned across the application and Settings navigation.
423
+
424
+ ## Development logging
425
+
426
+ `pnpm dev` owns the ready banner and public URL; `APP_SERVER_START_LOG=false` suppresses the underlying listener announcement through the `env` of `server/config/server.ts`. Keep that mapping when editing deployment environment settings. Request starts, request headers and config diagnostics use DEBUG; the normal INFO output contains completion summaries. See the shared application development Skill for hosted logging and upgrade limits.
427
+
428
+ ## Runtime paths and application creation
429
+
430
+ `runtime.paths`, configuration context `paths`, and `app.paths` share one resolved `AppPaths` object. Use `paths.storage('...')`, `paths.database('...')`, or the corresponding directory fields. `AppPathOptions` is input only; application path policies run before the final object is created and configuration is loaded. Standalone entries declare the deployment root in `server/runtime.ts` so the server and CLI share persistent storage outside the compiled code directory.
431
+
432
+ `server/app.ts` calls `createAppFromRuntime(runtime)` to transfer configuration, paths, mode and Host logging policy and bind `runtime.app`. Keep Provider, middleware and route registration explicit and ordered; `startApplicationInScope` owns startup and shutdown binding.
package/CLAUDE.md ADDED
@@ -0,0 +1,9 @@
1
+ @AGENTS.md
2
+
3
+ For any UI work, start with `.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md` (from the application root), which decides the workflow and routes to the rest. Its styling rules are in `references/styling.md` beside it: which semantic Tailwind utilities to use so components respond to theme changes, and how to keep deliberate fixed-size exceptions explicit. The full color, font, size, spacing, radius and shadow token contract, and creating or editing theme presets, are in `references/theme.md`.
4
+
5
+ ## Compiled migration and seed manifests
6
+
7
+ The application build generates `.manifest.json` in each compiled migrations and seeds directory after server compilation, path rewriting, and `afterServerBuild` hooks. Keep the manifest generator in the build when customizing it. Plugins generate their own manifests when built; an application must not regenerate manifests for installed dependencies.
8
+
9
+ TypeScript and compiled JavaScript use the same source checksum for migration history, while the loader separately verifies emitted JavaScript. Marked JavaScript requires its manifest. For a database with old raw JavaScript checksums, first run the compiled representation with matching original output; verified legacy hashes are converted under the task lock. Unreproducible old output remains an error. Never edit historical migrations, and never edit the history table by hand, to resolve an upgrade failure; `pnpm nocobase db repair` is the supported way to realign a checksum you can account for, and `pnpm nocobase db redo` the way to re-run a migration whose branch is still unmerged.