Imported from nocobase/nocobase3 (
packages/plugins/app-plugin-authz-default-access/AGENTS.md). Install upstream withnpx skills add nocobase/nocobase3 --skill app-plugin-authz-default-access. Copyright stays with the author.
AGENTS.md
This is a NocoBase application plugin: a package published to a registry and installed into an application someone else assembled. That makes it a guest, and most of the rules below follow from it.
Adding a dependency
Where a package goes depends on who has to resolve the import, and there are three different answers.
| The import is reached from | Declare it in |
|---|---|
server/ or database/, at runtime |
dependencies |
client/, as a value import |
peerDependencies |
registry/ |
nothing — the application compiles it |
Tests, build scripts, or import type only |
devDependencies |
pnpm deps:check at the repository root enforces the server row and runs in CI.
Why the client row is different
A deployed server resolves its imports at runtime. An application's pnpm build generates dist/package.json from dependencies and installs from it. A server import declared only as a devDependency resolves in every development checkout and is absent exactly once — on the deployed server, as a bare Cannot find package naming nothing that points back at this manifest.
An installing application resolves your client imports at build time. Your client/ is not bundled by this plugin: build is tsc, so dist/client/*.js keeps its bare imports and the application's Vite build resolves them. That application has only what the published manifest declares, and npm does not publish devDependencies — so a client import left there fails with Could not resolve "…". It will not fail here, because a workspace install links every devDependency into this plugin's own node_modules, which is why this mistake reaches a registry before anyone sees it.
But that same server never requires a browser package. Declaring one as a dependency would install it into every deployment, where nothing loads it.
peerDependencies is what satisfies both: the application installs one shared copy for its Vite build, while a deployment sets autoInstallPeers: false and installs none of them. One declaration is enough — pnpm installs and links a peer here, so this plugin's own lint, tests, and build resolve it without a second entry.
Do not mark such a peer optional. An optional peer is not auto-installed anywhere, including in the application that needs it, which is the failure this arrangement exists to prevent. optional means the consumer may legitimately not need the package at all.
So hono in server/routes/ is a dependency, and sonner in client/ is a peer. A dynamic import() counts as a value import; import type does not, wherever it appears.
registry/ is the exception: it is source the application copies into itself and compiles there, against that application's own react and @/ alias. This plugin never resolves those imports at all, so declaring them would claim dependencies it does not have.
Prefer what the application already has
Before adding a client package, check whether packages/templates/app-template-default already declares it. Reusing that version means the application bundles one copy instead of resolving two, and it keeps this plugin from pinning a range the application then has to work around. Use catalog: for anything the repository catalog already names.
Runtime packages are peers, never dependencies
@nocobase/app-server, @nocobase/app-client, @nocobase/db, @nocobase/i18n, @nocobase/service-provider, @nocobase/queue, and every other @nocobase/app-plugin-* carry process-wide state — service tokens compared by object identity, React contexts, a job registry. A second copy splits that state, and nothing warns: the install succeeds, the build succeeds, and at runtime a demonstrably registered service reports Service "..." is not registered.
Declare each as a peerDependency — the published contract: "provide this, and provide exactly one". One declaration is enough; pnpm installs a peer and links it into this package's own node_modules, so lint, tests, and the build resolve it without a second entry to keep in step. pnpm peers:check enforces this. The generator already emits this shape for the capabilities you selected.
Contributing CLI commands
A plugin can add commands to an application's pnpm nocobase, and can ask an application to run a command during its pnpm build or pnpm dev. Both are declared in cli/index.ts through defineCliPlugin, and the cli capability generates that entry with one example command.
Commands here are static tooling: they read and write files and packages. They never start the application, so nothing in one may resolve a service or query the database — anything needing the running application is a server route or a job.
Build and dev hooks are for a plugin that has to produce something before the application can run. Declaring the step here rather than in each application's build script is what keeps it correct: it appears only where this plugin is registered, and disappears with it.
buildHooks: {
afterServerBuild: [{ label: 'Build artifacts', command: ['pnpm', 'nocobase', '<topic>', 'build'] }],
},
A hook command is any executable with its arguments, already split — no shell, so no quoting to get right, and no && or pipes. The stage names say what exists when the hook runs: beforeBuild (empty dist), afterClientBuild (dist/client), afterServerBuild (+ dist/server), afterBuild (the installed deployment tree), and beforeDev for pnpm dev.
Before you finish
pnpm --filter <this-package> lint
pnpm --filter <this-package> typecheck
pnpm --filter <this-package> test
pnpm --filter <this-package> build
Every server route owns and tests its own authentication and authorization boundary; mounting under /api authenticates nothing. Keep declarations, exports, dependencies, tests, README, and Plugin Skills aligned when capabilities change.
The repository root AGENTS.md covers the rest — package publishing, test layout, migrations, and the reasoning behind the rules summarized here.
Server resource base and database builds
Every Server plugin declaration requires an absolute baseDir. In server/plugin.ts, calculate it with path.resolve(import.meta.dirname, '..') using node:path. Migrations, Seeds, and Jobs resolve only relative to this directory; the same declaration under dist/server resolves compiled resources. Keep source and published exports aligned.
After compiling database tasks and finishing JavaScript rewriting, run nocobase-db-manifests from @nocobase/dev-config. Publish the generated .manifest.json alongside the marked JavaScript in each migrations and seeds directory. Do not edit historical migration sources or bypass checksums to accommodate compilation differences.