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

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 (249) 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 +434 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +485 -0
  10. package/cli/plugins.ts +8 -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 +37 -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 +156 -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 +198 -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/api.ts +8 -0
  165. package/server/config/app.ts +40 -0
  166. package/server/config/auth.ts +36 -0
  167. package/server/config/authorization.ts +18 -0
  168. package/server/config/caching.ts +21 -0
  169. package/server/config/database.ts +22 -0
  170. package/server/config/drive.ts +30 -0
  171. package/server/config/i18n.ts +17 -0
  172. package/server/config/index.ts +67 -0
  173. package/server/config/jobs.ts +27 -0
  174. package/server/config/logging.ts +23 -0
  175. package/server/config/notification.ts +11 -0
  176. package/server/config/queue.ts +28 -0
  177. package/server/config/scheduler.ts +23 -0
  178. package/server/config/secrets.ts +10 -0
  179. package/server/config/server.ts +26 -0
  180. package/server/config/session.ts +60 -0
  181. package/server/config/snowflake.ts +21 -0
  182. package/server/config/spa.ts +24 -0
  183. package/server/config/users.ts +11 -0
  184. package/server/config/workflow.ts +24 -0
  185. package/server/config.ts +24 -0
  186. package/server/embedded.ts +23 -0
  187. package/server/locales/en-US.ts +10 -0
  188. package/server/locales/index.ts +10 -0
  189. package/server/locales/zh-CN.ts +5 -0
  190. package/server/plugins.ts +37 -0
  191. package/server/providers/index.ts +5 -0
  192. package/server/routes/index.ts +6 -0
  193. package/server/runtime.ts +24 -0
  194. package/server/standalone.ts +29 -0
  195. package/server/tsconfig.json +4 -0
  196. package/tailwind.config.mjs +59 -0
  197. package/tests/components/app-layout-routes.test.tsx +132 -0
  198. package/tests/components/app-title.test.tsx +57 -0
  199. package/tests/components/auth-i18n.test.tsx +87 -0
  200. package/tests/components/back-button.test.tsx +80 -0
  201. package/tests/components/file-preview.test.tsx +101 -0
  202. package/tests/components/header-hover.test.tsx +87 -0
  203. package/tests/components/inbox.test.tsx +139 -0
  204. package/tests/components/language-switcher.test.tsx +298 -0
  205. package/tests/components/layout-containers.test.tsx +15 -0
  206. package/tests/components/layout-i18n.test.tsx +187 -0
  207. package/tests/components/loading.test.tsx +25 -0
  208. package/tests/components/navigation-menu.test.tsx +355 -0
  209. package/tests/components/page-harness.test.tsx +318 -0
  210. package/tests/components/primitive-labels.test.tsx +80 -0
  211. package/tests/components/sidebar-permissions.test.tsx +110 -0
  212. package/tests/components/sidebar-preference.test.tsx +42 -0
  213. package/tests/fixtures/theme-tokens.html +12 -0
  214. package/tests/fixtures/theme-tokens.tsx +111 -0
  215. package/tests/logic/account-permissions.test.tsx +204 -0
  216. package/tests/logic/app-locale-coverage.test.ts +136 -0
  217. package/tests/logic/app-server.test.ts +1589 -0
  218. package/tests/logic/asset-url-build.test.ts +106 -0
  219. package/tests/logic/client-auth.test.tsx +122 -0
  220. package/tests/logic/client-plugin-registry.test.ts +63 -0
  221. package/tests/logic/client-route-recovery.test.tsx +80 -0
  222. package/tests/logic/client-routes.test.ts +126 -0
  223. package/tests/logic/client-runtime.test.ts +112 -0
  224. package/tests/logic/client-settings.test.tsx +841 -0
  225. package/tests/logic/client-shell.test.tsx +466 -0
  226. package/tests/logic/client-theme.test.tsx +441 -0
  227. package/tests/logic/config.test.ts +197 -0
  228. package/tests/logic/dev-client-watch.test.ts +108 -0
  229. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  230. package/tests/logic/device-code-migration.test.ts +70 -0
  231. package/tests/logic/lifecycle.test.ts +221 -0
  232. package/tests/logic/plugin-commands.test.ts +69 -0
  233. package/tests/logic/plugin-page-authz.test.ts +53 -0
  234. package/tests/logic/route-navigation.test.ts +66 -0
  235. package/tests/logic/route-overlay.test.tsx +353 -0
  236. package/tests/logic/server-imports.test.ts +88 -0
  237. package/tests/logic/tailwind-sources.test.ts +151 -0
  238. package/tests/logic/template-package-name.test.ts +88 -0
  239. package/tests/logic/theme-preferences.test.ts +86 -0
  240. package/tests/logic/theme-tokens.test.ts +248 -0
  241. package/tests/logic/toaster.test.tsx +108 -0
  242. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  243. package/tests/setup/client-config.ts +27 -0
  244. package/tsconfig.json +11 -0
  245. package/tsconfig.node.json +12 -0
  246. package/tsconfig.server.json +19 -0
  247. package/vite.config.ts +92 -0
  248. package/vitest.config.ts +44 -0
  249. package/README.md +0 -3
package/README.MD ADDED
@@ -0,0 +1,485 @@
1
+ # NocoBase 3 Application
2
+
3
+ A full-stack NocoBase 3 application. The server runs on `@nocobase/app-server` and the browser client is React with Refine, shadcn/ui, and Tailwind CSS.
4
+
5
+ You own everything in this directory. Add pages, API routes, database tables, and business logic directly here — this is your application's source code, not a framework you extend from the outside.
6
+
7
+ ## Default and Examples
8
+
9
+ Default provides the application shell and product capabilities without example plugins or business demo pages. Application-owned routes start empty, and new databases receive no article sample data. Its built-in Users integration lists direct Authorization Permission Sets as application roles, while access assigned to all authenticated users remains separate. Use `app-template-examples` for article management and runnable plugin demonstrations.
10
+
11
+ Existing Default installations retain their data when example plugins are unregistered. If an installation already ran the article migration, retain its original migration sources as described in the [upgrade migration rules](.agents/skills/nocobase-app-upgrade/references/edge-cases.md#migrations) before upgrading. Do not erase migration history or delete existing data to make an upgrade start.
12
+
13
+ ## Getting started
14
+
15
+ Configure the application, then start the development server:
16
+
17
+ ```bash
18
+ pnpm nocobase config init
19
+ pnpm nocobase config check
20
+ pnpm dev
21
+ ```
22
+
23
+ `pnpm nocobase config init` writes `config.yml` from `config.example.yml`, keeping its comments and generating the first key of `secrets.keys`, which encrypts what the application stores as a secret and from which the sign-in and session keys are derived. It is only needed once; `pnpm dev` and `pnpm start` refuse to run until it has been. `pnpm nocobase config check` loads the configuration the way a start would, connects to any database other than SQLite, and reports what would stop the application from starting or quietly misbehave.
24
+
25
+ `pnpm dev` starts the API server and the Vite client together, picks free ports if the defaults are taken, and prints the URL to open. Use the printed URL rather than assuming a port.
26
+
27
+ ### Develop against another backend
28
+
29
+ Set `PROXY_TARGET_URL` to the backend application's public URL, including its mount path but excluding `/api`:
30
+
31
+ ```bash
32
+ # Example only: replace /main with the backend's actual public mount path.
33
+ PROXY_TARGET_URL=http://127.0.0.1:13000/main pnpm dev
34
+ ```
35
+
36
+ **`/main` is the fallback, not a required or fixed path.** Local development reads `APP_BASE_PATH` from the command-line environment, then `.env.local`, then `.env`, falling back to `/main` when unset. The remote path comes from `PROXY_TARGET_URL`; it is not inferred from the local `APP_BASE_PATH`. Read the target application's actual public URL before constructing this value. If it is mounted at the origin root, use the origin URL without a mount path.
37
+
38
+ For example, these applications can use different paths:
39
+
40
+ ```bash
41
+ APP_BASE_PATH=/local PROXY_TARGET_URL=http://127.0.0.1:13000/crm pnpm dev
42
+ # /local/api/users -> http://127.0.0.1:13000/crm/api/users
43
+ # /local/ws -> ws://127.0.0.1:13000/crm/ws
44
+ ```
45
+
46
+ This runs Vite with your local client source and proxies API and WebSocket requests to that application. It does not start a local backend, allocate its port, wait for its health endpoint, or watch server configuration/plugins. Existing `beforeDev` hooks still run to prepare client artifacts. Open the printed **Local** URL (the Vite port); **Backend** identifies the target.
47
+
48
+ The target can be another local project running `pnpm dev`, or an HTTP(S) deployment. Use its printed application URL, not its Vite URL. Local `APP_BASE_PATH` and the remote mount path may differ: local `/main/api/users` can map to remote `/crm/api/users`. Trailing slashes are accepted. Credentials, query strings, and fragments are not accepted in the target URL.
49
+
50
+ Requests use the remote backend's data and permissions, including writes. Only `<APP_BASE_PATH>/api` and `<APP_BASE_PATH>/ws` are proxied; application root callbacks and absolute URLs returned by the backend keep their original routing. Cookie domain/path scopes are adapted to the local application. HTTP and WebSocket requests whose Origin matches the local request's protocol and Host are forwarded with the target Origin, so browser authentication and realtime connections pass the backend's same-origin checks. Same-origin Referer URLs within the local app base are mapped to the target app base as well; requests without Origin keep it absent. Other origins and referers remain unchanged and subject to backend validation. HTTPS-only cookie requirements remain enforced, and authentication flows using explicit callback URLs still require appropriate callback configuration.
51
+
52
+ The variable can also be set in `.env` or `.env.local`; the command-line environment takes precedence. Restart `pnpm dev` after changing the target. Without this variable, development still starts both the local backend and Vite. Production builds ignore it.
53
+
54
+ Use `APP_SERVER_PORT` to choose the local application port in either development mode. With `PROXY_TARGET_URL` set, it selects Vite's preferred port (5173 when unset); without a proxy target, it selects the local backend's preferred port (13000 when unset), while Vite still starts from 5173. If the preferred port is occupied, development selects the next available port and prints the actual URL. The remote backend address is always taken from `PROXY_TARGET_URL`.
55
+
56
+ ```bash
57
+ APP_SERVER_PORT=13399 PROXY_TARGET_URL=http://127.0.0.1:13000/main pnpm dev
58
+ # Local: http://127.0.0.1:13399/main/ when 13399 is available.
59
+ # Replace /main with the remote application's actual mount path.
60
+ ```
61
+
62
+ ### Review your configuration
63
+
64
+ `pnpm nocobase config init` wrote `config.yml` from `config.example.yml`, with a generated `secrets.keys` key filled in. It gets you running on SQLite; adjust it for your situation:
65
+
66
+ - **Database.** SQLite needs no server and works as it is. Another one means installing its driver first — `pnpm add @nocobase/db-postgres`, and `pnpm remove @nocobase/db-sqlite` if nothing else uses SQLite — then `pnpm nocobase config init --dialect postgres --force` to rewrite the connection, and `pnpm nocobase config set` for its settings, with `--from-env` for the password so it stays out of the shell history. Which dialects the application can run on is decided by what it depends on, not by this file: a driver it does not have cannot be introduced from here.
67
+ - **Change a value** with `pnpm nocobase config set key=value`, which keeps the comments and refuses a section the application does not know. Values that differ per machine — `APP_SERVER_PORT`, `APP_SERVER_HOST`, `APP_PUBLIC_ORIGIN` — are better set in the environment, which is applied over this file; `config set` says when an environment variable overrides what it wrote.
68
+ - **Where the application is served.** `APP_BASE_PATH` is the public mount path, `/main` by default, so the development URL looks like `http://127.0.0.1:13000/main/`. `APP_SERVER_PORT` and `APP_SERVER_HOST` change what the server binds to.
69
+ - **Anything a feature you enable requires** — mail delivery, IM webhooks, storage. Every section is already here, commented out where it is optional.
70
+
71
+ `config.yml` is gitignored because it holds your `secrets.keys`. Deploy it through your secret management rather than committing it.
72
+
73
+ Keep the key: what was encrypted under it cannot be read without it. To rotate it, put a new key first in `secrets.keys` with a higher version and keep the old one after it, run `pnpm nocobase secrets rotate` (`pnpm nocobase secrets status` shows what is left to reseal), then remove the old key. Changing the current key signs everyone out.
74
+
75
+ To run against a production build instead:
76
+
77
+ ```bash
78
+ pnpm build
79
+ pnpm start
80
+ ```
81
+
82
+ ## Project structure
83
+
84
+ ```text
85
+ client/ Browser application
86
+ ├── routes.ts Your page routes
87
+ ├── pages/ Your page components
88
+ ├── components/ Your components
89
+ │ └── ui/ shadcn/ui primitives
90
+ ├── layouts/ App and Settings layouts
91
+ │ └── components/ Layout containers, navigation, branding and account controls
92
+ ├── routing/ Route rendering, access checks, loading and error UI
93
+ ├── locales/ Your translated strings
94
+ ├── theme/ App-wide theme and light/dark selector
95
+ ├── extensions/ Application-owned copies of plugin-published UI
96
+ ├── plugins.ts Which plugins the browser loads
97
+ ├── react-providers.ts Your React context providers
98
+ ├── service-provider.ts Your client startup logic
99
+ └── runtime.ts Composition root tying the above together
100
+
101
+ server/ API server
102
+ ├── routes/ Your HTTP endpoints
103
+ ├── providers/ Your services and their lifecycle
104
+ ├── config/ Application configuration composition
105
+ ├── plugins.ts Which plugins the server loads
106
+ ├── app.ts Application assembly
107
+ ├── runtime.ts Composition root
108
+ ├── standalone.ts Node entry point
109
+ └── embedded.ts Entry point when a host process mounts it
110
+
111
+ cli/ Command line
112
+ ├── plugins.ts Which plugins contribute commands and build and dev hooks
113
+ └── commands/ Your own commands, under the app topic (created when needed)
114
+
115
+ database/
116
+ └── main/
117
+ ├── migrations/ Your schema history (initially empty)
118
+ └── seeds/ Your required initial data (initially empty)
119
+
120
+ tests/ Unit and integration tests, run by Vitest
121
+ └── playwright/ Browser tests against a running application, run by Playwright
122
+ config.yml Your configuration, written by pnpm nocobase config init; gitignored
123
+ config.example.yml Documented configuration template
124
+ ```
125
+
126
+ The three files named `runtime.ts`, `plugins.ts`, and `routes.ts` are composition roots: they list what the application is made of. Reading them tells you what is loaded and in what order.
127
+
128
+ ## Scripts
129
+
130
+ `pnpm dev`, `pnpm build` and `pnpm start` are the application's scripts. Everything else is `pnpm nocobase <topic> <command>`, which pnpm resolves to the application's CLI without a script; `pnpm nocobase --help` lists them all.
131
+
132
+ ### Everyday development
133
+
134
+ | Script | What it does |
135
+ | ------------------ | ------------------------------------------------------------------------------------ |
136
+ | `pnpm dev` | Start the API server and client with hot reload |
137
+ | `pnpm build` | Build the client and server into `dist/` |
138
+ | `pnpm start` | Run the built server; requires `pnpm build` first |
139
+ | `pnpm build --tar` | Build, then pack `dist/` and `config.example.yml` into `storage/exports/dist.tar.gz` |
140
+
141
+ ### Database
142
+
143
+ | Command | What it does |
144
+ | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
+ | `pnpm nocobase db apply` | Apply pending migrations from your `database/main/migrations/` and from every registered plugin, then pending seeds from `database/main/seeds/` and from those plugins |
146
+ | `pnpm nocobase db reset` | Destructive: drop every managed schema object, then rerun all migrations and seeds from empty; asks first, and needs `--force` in CI |
147
+ | `pnpm nocobase db repair` | Realign recorded migration and seed checksums with the current sources; `--dry-run` reports without writing |
148
+ | `pnpm nocobase collections generate` | Write `collection.json`, `metadata.json` and `schema.json` for every Collection under `database/<connection>/collections/`, plus a `_manifest.json`; `--check` fails when the files are out of date |
149
+
150
+ Migrations and seeds also run automatically on startup while `database.connections.main.migrations.autoRun` is `true` in `config.yml`. Run `pnpm nocobase db apply` explicitly when you want to apply a change without restarting. Both halves run only what is pending, so running it on an already-migrated database applies seeds alone.
151
+
152
+ Database tasks support `--connection <name>` and `--all`; no argument targets `database.default`. Per-connection source and upgrade rules are documented in [migrations and seeds](.agents/skills/nocobase-app-development/references/migrations.md).
153
+
154
+ ### Checks
155
+
156
+ | Script | What it does |
157
+ | -------------------- | ------------------------------------------------------------ |
158
+ | `pnpm test` | Run the test suite once |
159
+ | `pnpm test:watch` | Re-run tests as files change |
160
+ | `pnpm test:coverage` | Run tests and write a coverage report |
161
+ | `pnpm typecheck` | Typecheck the client, tooling, and server |
162
+ | `pnpm lint` | Report lint problems |
163
+ | `pnpm fix` | Fix lint problems, then format |
164
+ | `pnpm check` | Everything above plus a build; what to run before committing |
165
+
166
+ `pnpm nocobase locales check` reports a language declared in `client/locales/` but not `server/locales/`, or the reverse, and exits nonzero until the lists align. The browser builds its language picker from the client list, so a client-only language still works in the interface; server messages fall back to English and the language control explains that fallback. Add the matching server locale when server-produced text should use the same language.
167
+
168
+ ### Plugins
169
+
170
+ Plugins add features to your application. Let these commands edit `package.json`, `client/plugins.ts`, and `server/plugins.ts` for you rather than editing those files by hand.
171
+
172
+ | Command | What it does |
173
+ | ---------------------------------------- | ----------------------------------------------------------- |
174
+ | `pnpm nocobase plugin register <name>` | Install a plugin and wire it into the client and server |
175
+ | `pnpm nocobase plugin unregister <name>` | Remove a plugin and its wiring |
176
+ | `pnpm nocobase plugin update [name]` | Upgrade one or all registered plugins and re-sync skills |
177
+ | `pnpm nocobase plugin inspect <name>` | Report a plugin's registration state; read-only |
178
+ | `pnpm nocobase package remove <name>` | Remove a direct NocoBase package and its synchronized Skill |
179
+ | `pnpm nocobase skills sync` | Sync direct NocoBase package Skills into `.agents/skills/` |
180
+
181
+ `pnpm install` automatically runs `pnpm nocobase skills sync` through the application's `postinstall` hook. Run `pnpm nocobase skills sync` manually when install scripts were disabled or Skills need refreshing.
182
+
183
+ Use a full package name, as with `plugin register` and `plugin unregister`; short names such as `authentication` also work. The optional name is a positional argument, not `--plugin`.
184
+
185
+ ```bash
186
+ pnpm nocobase plugin update @nocobase/app-plugin-authentication # Update one registered plugin
187
+ pnpm nocobase plugin update authentication # Equivalent short name
188
+ pnpm nocobase plugin update # Update all registered plugins
189
+ pnpm nocobase plugin update @nocobase/app-plugin-authentication --dry-run # Preview only
190
+ ```
191
+
192
+ Without a name, the command collects registered plugins from `client/plugins.ts`, `server/plugins.ts`, and `cli/plugins.ts`. It uses the application's package manager; with pnpm, `update` follows the declared version ranges without `--latest`. An unregistered name is rejected, and an application with no registered plugins is left unchanged. After a successful package update, all registered plugins' agent skills are re-synchronized, even when only one package was selected.
193
+
194
+ Ordering in `client/plugins.ts` and `server/plugins.ts` is contribution order, and a plugin is enabled by appearing in the array. Reorder entries or pass a plugin its options by hand; leave adding and removing entries to the commands.
195
+
196
+ ### Removing a NocoBase package
197
+
198
+ Before removing a direct `@nocobase/*` dependency, search the application's imports, plugin registrations, routes, services, configuration keys, tests, and build scripts for references to it. Migrate or remove those references first. The removal command updates package metadata and synchronized Skills; it does not rewrite application source or configuration and cannot decide whether the capability is still required.
199
+
200
+ ```bash
201
+ pnpm nocobase package remove @nocobase/example
202
+ pnpm nocobase package remove @nocobase/example --dry-run
203
+ pnpm nocobase package remove @nocobase/example --json
204
+ ```
205
+
206
+ For an ordinary package, the command uses the application's package manager to remove the dependency and update both `package.json` and the lockfile, then removes only the Skill directories recorded as owned by that package. For an `@nocobase/app-plugin-*` package, it reuses the `plugin unregister` workflow so Client, Server, and CLI registrations are removed together with the dependency and its Skills. `pnpm nocobase plugin unregister <name>` remains available when the task is specifically expressed as unregistering a plugin.
207
+
208
+ After an interrupted or manual removal, make sure the manifest no longer declares the package and run a full `pnpm nocobase skills sync` to reconcile stale package-owned output. Passing a package already absent from the manifest to `package remove` can clean its recorded historical Skill ownership without uninstalling any other package.
209
+
210
+ ### Diagnostics
211
+
212
+ | Command | What it does |
213
+ | ----------------------------- | ------------------------------------------------------------------------------------------- |
214
+ | `pnpm nocobase dist retarget` | Replace native binaries with the target platform's; part of `pnpm build` |
215
+ | `pnpm nocobase dist check` | Check that everything your server and CLI import reaches a deployment; part of `pnpm build` |
216
+
217
+ #### Deploying to a different machine
218
+
219
+ `pnpm build` produces `dist/`, which is the whole deployment: copy it to a server and run it. The build already ran `pnpm install --prod` inside `dist/`, so the tree ships with its `node_modules` and needs no install step on the server. It also keeps its own `package.json`, so `pnpm install --prod` run inside `dist/` on the server reinstalls exactly the same set: useful when `node_modules` was left out of a copy, never required after a normal build.
220
+
221
+ Client packages are not among them. Plugins declare what their browser code needs as peer dependencies, so your application installs one shared copy for its Vite build, while `dist/` opts out of peer installation entirely — `lucide-react`, `@base-ui/react` and the rest never reach a server that has no browser in it.
222
+
223
+ **The build targets the machine you run it on**, so `pnpm build && pnpm start` works. Deploying elsewhere means saying where:
224
+
225
+ ```bash
226
+ pnpm build --help # all build options
227
+ pnpm build # this machine
228
+ pnpm build --target linux-x64 # most Linux servers
229
+ pnpm build --target linux-arm64 # ARM servers, Graviton
230
+ pnpm build --target linux-x64-musl # Alpine, and most slim container images
231
+ pnpm build --target linux-x64 --node-version 24 # explicitly target Node 24
232
+ ```
233
+
234
+ A `.node` binary is compiled for one platform, architecture, C library, and Node version at once, so a build made on a Mac installs binaries a Linux server cannot load. `--target` sets platform, architecture, and C library together, because they are chosen together: `linux-x64` and `linux-x64-musl` are different binaries, and Alpine needs the musl one. If you are unsure, `ldd --version` on the server prints `musl` for musl and `GNU libc` otherwise.
235
+
236
+ `--node-version` takes the major version your server runs, separately, because the same Linux host may run any of several. The binary retargeter knows ABI mappings for 20, 22, 24, and 26; the application itself requires Node >=24.
237
+
238
+ `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.
239
+
240
+ For example, `pnpm build --target linux-x64 --node-version 24` produces:
241
+
242
+ ```json
243
+ {
244
+ "nocobase": {
245
+ "buildTarget": {
246
+ "platform": "linux",
247
+ "arch": "x64",
248
+ "libc": "glibc",
249
+ "nodeMajor": 24,
250
+ "nodeAbi": 137
251
+ }
252
+ }
253
+ }
254
+ ```
255
+
256
+ `nodeMajor` describes the target runtime, not the Node version used to run the build. No target patch version is implied. These fields provide platform and Node ABI checks for Hub deployment; they do not describe system libraries, CPU instruction requirements, or other runtime prerequisites.
257
+
258
+ If your application has no native modules — `pg`, `mysql2`, and `tedious` are all plain JavaScript — no platform-specific binaries need retargeting. The build still records the selected target, and the deployment runtime must satisfy `engines.node`.
259
+
260
+ #### Serving static assets from a CDN
261
+
262
+ Set `CDN_BASE_URL` when building to use a CDN for Vite-generated asset URLs:
263
+
264
+ ```bash
265
+ CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build
266
+ ```
267
+
268
+ Use a full HTTPS URL ending in `/`. The build reads the process environment first, then `.env.local`, then `.env`. Unset, empty or whitespace-only values keep the default relative base (`./`). Development ignores this variable. This is a build parameter, not a `config.yml` option or a runtime variable listed by `pnpm nocobase config variables` or `dist/variables.json`; changing it requires rebuilding.
269
+
270
+ Upload the static files in `dist/client/` to the configured CDN prefix, preserving their paths. For example, `dist/client/assets/index-abc.js` must be available at `https://cdn.example.com/my-app/v1/assets/index-abc.js`. Configure the CDN to allow cross-origin module and font requests from the application origin. A release-specific prefix keeps different builds separate; retain assets needed by clients still running an older release.
271
+
272
+ Keep the complete `dist/client/` tree on the application server, and serve the application's HTML through it so runtime configuration is injected. The CDN setting changes Vite-processed HTML, JavaScript and CSS resource URLs, and static files addressed with `resolveAssetUrl`, including this template's logos:
273
+
274
+ ```tsx
275
+ import { resolveAssetUrl } from '@nocobase/app-client';
276
+
277
+ <img src={resolveAssetUrl('/assets/logo-mark.png')} alt='Logo' />;
278
+ ```
279
+
280
+ Pass the file's path relative to `dist/client/`, with or without a leading slash. `resolveAssetUrl` reads `import.meta.env.BASE_URL`, which Vite replaces with the configured `base` during the application build; the browser does not read `CDN_BASE_URL` from the server environment. With an absolute HTTP(S) CDN base it returns the CDN URL, otherwise it falls back to `resolveAppUrl` and the runtime `APP_BASE_PATH`. Already resolved URLs are preserved. Use `resolveAppUrl` for API URLs, page links and runtime-generated resources such as workflow artifacts; the CDN must actually contain a file before it can be served there.
281
+
282
+ #### Packing the build
283
+
284
+ `pnpm build --tar` writes `storage/exports/dist.tar.gz`, the artifact you upload to a Hub to deploy this application.
285
+
286
+ The archive has two levels: `config.example.yml` and `dist/` sit at the root, and the build is inside `dist/`. What `dist/` holds varies with the application; this is the shape:
287
+
288
+ ```text
289
+ dist.tar.gz
290
+ ├── config.example.yml
291
+ └── dist/
292
+ ├── cli/
293
+ ├── client/
294
+ ├── node_modules/
295
+ ├── server/
296
+ ├── package.json
297
+ └── pnpm-workspace.yaml
298
+ ```
299
+
300
+ Combine it with `--target` when the archive is for another machine:
301
+
302
+ ```bash
303
+ pnpm build --target linux-x64 --tar
304
+ ```
305
+
306
+ #### Building a Docker image
307
+
308
+ For a source build using a CDN, pass its URL as a build argument:
309
+
310
+ ```bash
311
+ docker build --build-arg CDN_BASE_URL=https://cdn.example.com/my-app/v1/ -t my-app .
312
+ ```
313
+
314
+ With `DIST=prebuilt`, set `CDN_BASE_URL` on the earlier `pnpm build` command instead: the image packages the existing client files unchanged. Setting `CDN_BASE_URL` on a prebuilt image build or with `docker run -e` cannot change their compiled URLs. The source build does not read the host's `.env`; use the build argument.
315
+
316
+ `Dockerfile` builds this application from its sources into a production image. From the project root:
317
+
318
+ ```bash
319
+ docker build -t my-app .
320
+ ```
321
+
322
+ The build runs `pnpm build` inside the container, and the image holds only `dist/` and `config.example.yml`. `Dockerfile.dockerignore` keeps `config.yml`, `.env`, `storage/`, and `node_modules` out of the build context; keep the two files together. Settings in `.env` do not reach the image, so pass them to the container as environment variables.
323
+
324
+ The client is not tied to a mount path, so choose it when starting the container, `-e APP_BASE_PATH=/crm`; the image defaults to `/main`. The build runs on the builder's own architecture and cross-targets native modules, so `docker buildx build --platform linux/amd64,linux/arm64` compiles nothing under emulation.
325
+
326
+ To package a `dist/` you have already built instead, build it for the image's platform and pass `DIST=prebuilt`. Nothing is installed or compiled in the image, and it works in any directory that holds the build:
327
+
328
+ ```bash
329
+ pnpm build --target linux-x64
330
+ docker build --platform linux/amd64 --build-arg DIST=prebuilt -t my-app .
331
+ ```
332
+
333
+ `--target` and `--platform` must name the same architecture: Docker otherwise builds for the machine it runs on, which on Apple silicon is `linux/arm64`. The image build fails unless `dist/` was built for `linux` with glibc, the image's architecture, and Node 24, and unless it records `nocobase.relocatable`, which an `@nocobase/app-cli` from before relocatable builds does not. `dist/.env`, which `pnpm build` fills from your local `.env` files, is never copied in. One `dist/` covers one architecture, so a multi-platform image has to be built from source.
334
+
335
+ Run it with the configuration and data mounted, never copied in:
336
+
337
+ ```bash
338
+ docker run -d --init -p 13000:13000 \
339
+ -v ./config.yml:/app/config.yml:ro \
340
+ -v ./storage:/app/storage \
341
+ my-app
342
+ ```
343
+
344
+ The image runs as the `node` user without pnpm, so `storage/` must be writable by UID 1000, and application commands run through `node dist/cli/index.js`, as in `docker run --rm -v ./config.yml:/app/config.yml:ro my-app node dist/cli/index.js config check`.
345
+
346
+ #### Adding a dependency
347
+
348
+ Where a package goes depends on which half of your application imports it.
349
+
350
+ | Imported from | Declare it in |
351
+ | --------------------------------- | ----------------- |
352
+ | `server/`, `database/`, or `cli/` | `dependencies` |
353
+ | `client/`, build tooling, tests | `devDependencies` |
354
+
355
+ `dist/package.json` is generated from `dependencies` and is what a server installs, so a package in `devDependencies` is missing there however well it works locally. Client packages need nothing at runtime — Vite resolves and inlines them into `dist/client` while building.
356
+
357
+ Plugins' browser packages need nothing from you: a plugin declares those as peer dependencies, so installing the plugin brings one shared copy into your application, and `dist/` never installs them.
358
+
359
+ `pnpm build` verifies the server half and fails the build if something is missing, naming the package. One case it cannot check is an import whose name is assembled while the program runs:
360
+
361
+ ```ts
362
+ await import(`${name}/index.js`);
363
+ ```
364
+
365
+ Nothing can tell in advance which package that names, so declare it in `dependencies` when you write the code.
366
+
367
+ #### When the build or the deployment goes wrong
368
+
369
+ **`Cannot find module` on the server, but it runs locally.** The package is in `devDependencies`, which `dist/package.json` is not generated from. Move it to `dependencies` and rebuild.
370
+
371
+ **A browser package fails to resolve while building your application.** A plugin declares it as a peer dependency and something has to provide it. Add it to your `devDependencies`.
372
+
373
+ **`Error loading shared library`, `invalid ELF header`, or a bare reference to a `.node` file at startup.** The binary does not match the server. Compare what the build targeted against the server:
374
+
375
+ ```bash
376
+ node -e "console.log(require('./dist/package.json').nocobase.buildTarget)"
377
+ node -p "process.platform + '-' + process.arch" # on the server
378
+ node -p "process.versions.modules" # on the server: the ABI
379
+ ```
380
+
381
+ Rebuild with a matching `--target` and `--node-version`. ABI 115 is Node 20, 127 is Node 22, 137 is Node 24, 147 is Node 26.
382
+
383
+ **`no prebuilt binary for <target>` during the build.** The package publishes no build for that combination. Check whether the version you depend on supports the target, and whether a musl build exists if you asked for one — musl coverage is thinner than glibc.
384
+
385
+ ## Configuration
386
+
387
+ `server/config/` and `client/config/` contain editable TypeScript defaults. `config.yml` holds environment-specific overrides, written by `pnpm nocobase config init` and yours to edit from there. It is gitignored because it carries your `secrets.keys`; deploy it through your secret management rather than committing it. It is generated from `config.example.yml`, which is kept beside it as the reference for every option.
388
+
389
+ Two settings decide where the application is reachable:
390
+
391
+ - `APP_BASE_PATH` is the public path the application is mounted at, `/main` by default. The browser API base URL is derived from it as `<APP_BASE_PATH>/api`.
392
+ - `APP_PUBLIC_ORIGIN` is the external origin used to build callback and redirect URLs. Set it in production. It must not repeat the base path — the runtime joins the two.
393
+
394
+ An environment variable a section declares takes precedence over the file, and the file over the code defaults. `pnpm nocobase config env` lists the variables and whether each is set; `pnpm nocobase config variables` describes each one — what it sets, whether it is a secret, whether a deployment must supply it — and `pnpm build` writes that description to `dist/variables.json`. `DB_*` set the main database connection, `INITIAL_ADMIN_USERNAME`, `INITIAL_ADMIN_EMAIL` and `INITIAL_ADMIN_PASSWORD` the first administrator, and `APP_SAMPLE_DATA=true` loads the sample data when the database is first installed. `${NAME}` in `config.yml` is not expanded.
395
+
396
+ ## Testing
397
+
398
+ Tests live in `tests/`, never beside the source they cover. Put logic and integration tests under `tests/logic/` and component tests under `tests/components/`. Browser tests belong in `tests/playwright/`, which Vitest skips. Playwright does not start the application: start it first (`pnpm dev`, or `pnpm build` and then the built application), then run `pnpm test:e2e`; each test reads the application URL, including its base path, from `APP_URL`. Tests that only need a real server, real authentication, or a real database run under Vitest with `createAppTest()` from `@nocobase/app-testing/server`.
399
+
400
+ ### Notification testing
401
+
402
+ After signing in with the `notification:test` `send` permission, use the
403
+ notification logs settings page to send a real message through the durable
404
+ Notification Manager. The form is generated from safe Channel test metadata;
405
+ Email tests require an address and IM tests use the configured logical target.
406
+ The button remains visible so configuration problems are discoverable. The
407
+ server enforces the `notification:test` `send` permission only when the message
408
+ is submitted.
409
+
410
+ ## API documentation
411
+
412
+ The application documents every `/api` endpoint it serves — its own routes, every registered plugin's, the data endpoints and Better Auth's sign-in endpoints — in an OpenAPI 3.1 document:
413
+
414
+ - Swagger UI: `<origin><APP_BASE_PATH>/api/swagger/docs`, such as `http://127.0.0.1:13000/main/api/swagger/docs`
415
+ - JSON: `<origin><APP_BASE_PATH>/api/swagger`
416
+
417
+ Only a signed-in user or an API key may read them; anyone else gets `401`. Open the Swagger UI in a browser where you are signed in, or, from a script, send an API key created at `<APP_BASE_PATH>/settings/api-keys` in the `x-api-key` header. Swagger UI's **Authorize** button takes a key too and keeps it across reloads.
418
+
419
+ ```bash
420
+ curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
421
+ ```
422
+
423
+ Replace `/main` with the application's `APP_BASE_PATH`. An application without the authentication plugin registers no way to tell who is asking, so it does not serve the documentation at all and both URLs answer `404`. There is no setting that makes it public.
424
+
425
+ ## Optional Workflow plugin
426
+
427
+ The Workflow plugin is disabled by default in Client, Server, and CLI. Its package dependency and `server/config/workflow.ts` are retained. To opt in, register the plugin with `pnpm nocobase plugin register @nocobase/app-plugin-workflow` and restore the `workflow` import, type, and entry in `server/config/index.ts`.
428
+
429
+ ## Working with an AI agent
430
+
431
+ `AGENTS.md` describes how to build features here, and `.agents/skills/` contains package-owned guidance generated by `pnpm nocobase skills sync`. Give an agent an API key when it needs to call the application: it learns the available endpoints from the API document above rather than from route sources. If you use Claude Code, `CLAUDE.md` points at the same guidance. Keep application-specific rules in committed `AGENTS.md` files because synchronization replaces package-owned Skill directories.
432
+
433
+ ## License
434
+
435
+ MIT
436
+
437
+ ## Publish to an application Hub
438
+
439
+ This template depends on `@nocobase/hub-cli`, which gives the application the `pnpm nocobase hub` commands; removing the dependency removes them. Run them in the source checkout or in CI: a built `dist/` does not register them, because they build and send the application from its sources.
440
+
441
+ ### Add the Hub as a remote
442
+
443
+ A remote is one App on one Hub, addressed as `<Hub URL>/apps/<App ID>`, where the Hub URL includes the Hub's mount path:
444
+
445
+ ```bash
446
+ pnpm nocobase hub remote add origin http://127.0.0.1:13000/main/apps/crm
447
+ pnpm nocobase hub remote list
448
+ ```
449
+
450
+ Remotes are saved in `.nocobase/hub.json`, which holds addresses only and belongs in version control. The first remote added is the default; every other `hub` command takes `--remote <name>` to pick another, and `hub remote remove <name>` deletes one.
451
+
452
+ ### Create and save a publishing key
453
+
454
+ The `hub` commands authenticate with a Hub publishing key, created in Hub rather than in this application. Open **API Keys** in the Hub navigation (`<Hub URL>/api-keys`); reaching that page requires `hub.app / manage-api-keys`, granted to `hub-administrator` and `hub-operator` by default. In **Create API Key**, select this application under **Applications**, or **All applications (including future apps)**, then grant the permissions the commands below need: **Upload release** for `pnpm nocobase hub upload`, and both **Upload release** and **Deploy release** for `pnpm nocobase hub deploy`.
455
+
456
+ The plaintext key is shown once at creation; while the key stays active its creator can copy it again from the same page. Bound applications and granted permissions cannot be changed afterwards, so a key created without `Deploy release` has to be deleted and recreated rather than edited. A key never exceeds its creator's current permissions, so uploads fail if the creator later loses access to the bound application. See [Publish applications with Hub](https://github.com/nocobase/nocobase/blob/v3-develop/docs/docs/en/deployment/hub.md) for the console walkthrough.
457
+
458
+ Save the key with `pnpm nocobase hub auth login`, which asks for it without echoing it and checks with the Hub that it opens the App. It is stored outside the project in `$XDG_CONFIG_HOME/nocobase/hub-credentials.json` (`~/.config` when that is unset, `%APPDATA%\nocobase` on Windows), readable by the owner only, one key per remote URL. The commands read no key or Hub address from `.env`, the environment or command-line flags. `hub auth status` reports whether each remote has a key the Hub accepts, and `hub auth logout` removes a saved key. In CI, pipe the key in from a secret:
459
+
460
+ ```bash
461
+ echo "$HUB_KEY" | pnpm nocobase hub auth login --remote origin --with-token
462
+ pnpm nocobase hub deploy --remote origin --json
463
+ ```
464
+
465
+ ### Deploy
466
+
467
+ ```bash
468
+ pnpm nocobase hub deploy --json
469
+ pnpm nocobase hub upload --json
470
+ pnpm nocobase hub deploy --release-id <releaseId> --json
471
+ ```
472
+
473
+ `hub deploy` asks the Hub which platform it runs Apps on, builds the application for it with `nocobase build --target … --node-version … --tar`, uploads the archive as a Release and deploys it, waiting for the result unless `--no-wait` is given. Do not run `pnpm build --tar` first. When the Hub already has the archive, its Release is deployed. `hub upload` builds and uploads without deploying, and `hub deploy --release-id` deploys a Release already on the Hub, which is also how to roll back. `--no-build` sends the existing `storage/exports/dist.tar.gz` and `--file` another archive; both are checked against the Hub's platform and fail with `BUILD_TARGET_MISMATCH` when they were built for another. Hub has no automatic or manual deployment setting; scripts decide which commands to run.
474
+
475
+ `node_modules/@nocobase/hub-cli/README.md` is the full reference: every flag, `--config` for the runtime configuration, retry identity, the `--json` document and the exit codes. Exit 3 does not mean the deployment failed or was cancelled; inspect the deployment in Hub and retry with the same `--idempotency-key` if necessary.
476
+
477
+ ## Runtime paths and application creation
478
+
479
+ `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. That storage is `storage/` under the deployment root unless `APP_STORAGE_DIR` names another directory, absolute or relative to the deployment root; set it when the deployment root itself is replaced on every release, as an installer that keeps one directory per release does.
480
+
481
+ `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.
482
+
483
+ ## Strict startup verification
484
+
485
+ Set `NOCOBASE_STRICT_STARTUP=true` when running `pnpm dev` or `pnpm start` in automated verification. Startup failures, including job import failures, exit nonzero after resource cleanup. Strict dev runs the server without watch mode so a failed server cannot remain hidden behind a watcher; restart the command after server or configuration changes. Client HMR remains available. Omit the variable or set it to `false` for normal development with server hot reload. Request errors and individual job execution failures do not terminate the application.
package/cli/plugins.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineCliPlugins, type AppCliPlugins } from '@nocobase/app-cli';
2
+ import scheduler from '@nocobase/app-plugin-scheduler/cli';
3
+
4
+ // Array order is command registration order. A plugin contributes its commands
5
+ // by appearing in this list; removing its entry and its import removes them.
6
+ const cliPlugins: AppCliPlugins = defineCliPlugins([scheduler]);
7
+
8
+ export default cliPlugins;
@@ -0,0 +1,19 @@
1
+ import { defineClientExtension } from '@gchust/agent-annotations/extension';
2
+
3
+ export default defineClientExtension({
4
+ id: 'nocobase.theme',
5
+ apiVersion: 1,
6
+ host: {
7
+ theme: () =>
8
+ document.documentElement.classList.contains('dark') ? 'dark' : 'light',
9
+ brandColor: () => '#6d28d9',
10
+ subscribe(listener) {
11
+ const observer = new MutationObserver(listener);
12
+ observer.observe(document.documentElement, {
13
+ attributeFilter: ['class'],
14
+ attributes: true,
15
+ });
16
+ return () => observer.disconnect();
17
+ },
18
+ },
19
+ });
package/client/app.ts ADDED
@@ -0,0 +1,38 @@
1
+ import {
2
+ ClientApplication,
3
+ defineAppClientRenderConfig,
4
+ type AppClientRenderConfig,
5
+ } from '@nocobase/app-client';
6
+ import { I18nProvider } from '@nocobase/i18n/client';
7
+ import type { AppRuntimeContext } from '@nocobase/app-client/runtime';
8
+ import {
9
+ createElement,
10
+ type PropsWithChildren,
11
+ type ReactElement,
12
+ } from 'react';
13
+
14
+ import { AppRouter } from './routing/app-router.js';
15
+ export function createApp(runtime: AppRuntimeContext): ClientApplication {
16
+ // Outermost, so every provider and page below can translate.
17
+ const AppI18nProvider = ({ children }: PropsWithChildren): ReactElement =>
18
+ createElement(I18nProvider, { runtime: runtime.i18n }, children);
19
+
20
+ const app = new ClientApplication({
21
+ runtime,
22
+ createRenderConfig: (): AppClientRenderConfig =>
23
+ defineAppClientRenderConfig({
24
+ basename: runtime.basename,
25
+ reactProviders: [
26
+ AppI18nProvider,
27
+ ...runtime.reactProviders.map((provider) => provider.component),
28
+ ],
29
+ routes: createElement(AppRouter, {
30
+ clientRoutes: runtime.routes,
31
+ settingsRouteTree: runtime.settingsRouteTree,
32
+ devRouteTree: runtime.devRouteTree,
33
+ }),
34
+ }),
35
+ });
36
+ runtime.app = app;
37
+ return app;
38
+ }
@@ -0,0 +1,49 @@
1
+ import { useTranslation } from '@nocobase/i18n/client';
2
+ import { ArrowLeftIcon } from 'lucide-react';
3
+ import type { ReactElement, ReactNode } from 'react';
4
+ import { Link, type To, useLocation } from 'react-router';
5
+
6
+ import { cn } from 'cn';
7
+
8
+ export interface BackButtonProps {
9
+ /**
10
+ * Where it leads. Defaults to the parent route with the current query string, as closing a route overlay does, so
11
+ * the list a child page covers keeps its search and filters.
12
+ */
13
+ readonly to?: To;
14
+ /** The label, "Back" by default. */
15
+ readonly children?: ReactNode;
16
+ readonly className?: string;
17
+ }
18
+
19
+ /**
20
+ * The way back from a page that sits below another one: a covering child page, a record's own page, a form too long
21
+ * for a dialog.
22
+ *
23
+ * It stands on its own, above the page's heading where breadcrumbs would otherwise be, and needs no `PageHeader`.
24
+ * Going back is navigation, so it is a muted text link with an arrow, turning to the foreground on hover: no button
25
+ * chrome, so it reads as a way out rather than an action. It replaces the history entry, as closing a route overlay
26
+ * does, so the browser's Back does not return to the page just left, such as a form that would reopen empty.
27
+ */
28
+ export function BackButton({
29
+ children,
30
+ className,
31
+ to,
32
+ }: BackButtonProps = {}): ReactElement {
33
+ const { t } = useTranslation();
34
+ const location = useLocation();
35
+
36
+ return (
37
+ <Link
38
+ replace
39
+ className={cn(
40
+ 'inline-flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground',
41
+ className,
42
+ )}
43
+ to={to ?? { pathname: '..', search: location.search }}
44
+ >
45
+ <ArrowLeftIcon className='size-4' />
46
+ {children ?? t('navigation.back', { defaultValue: 'Back' })}
47
+ </Link>
48
+ );
49
+ }