This guide starts from an App generated by create-app and explains where local plugin files live, what they do and how they connect to the application. Plugins live under the App’s packages/ directory. Their structure depends on the selected capabilities; not every plugin needs Client, Server and CLI entries.
Where the plugin lives in an App
Consider an announcements plugin that provides a page and an API for reading announcements. These are its main files after implementation and registration in the App; common configuration and some tests are omitted. client/pages/, shared/ and the page components are added to the scaffold. See Write your first plugin for the implementation.
my-app/
├── client/plugins.ts # App registers the Client entry
├── server/plugins.ts # App registers the Server entry
├── packages/
│ └── app-plugin-announcements/
│ ├── client/
│ │ ├── components/ # Page container and header components
│ │ ├── pages/announcements.tsx # Page: requests and displays announcements
│ │ ├── locales/ # Menu and page translations
│ │ ├── routes.ts # Page route and navigation
│ │ ├── plugin.ts # Composes Client contributions
│ │ └── index.ts # Public Client entry
│ ├── server/
│ │ ├── routes/index.ts # Announcement API
│ │ ├── routes/schemas.ts # API response schema
│ │ ├── plugin.ts # Composes Server contributions
│ │ └── index.ts # Public Server entry
│ ├── shared/announcement.ts # Shared data type
│ ├── tests/ # API, page and route tests
│ ├── package.json # Dependencies, exports and scripts
│ └── AGENTS.md # Plugin development guidance
├── package.json # References the plugin through workspace:*
└── pnpm-workspace.yaml # Includes the plugin directoryWhen the page opens, client/pages/announcements.tsx makes a request, server/routes/index.ts returns announcements, and the page displays the list. The page and API belong to the same plugin. This example reads fixed sample data, so it needs neither plugin database migrations nor a CLI entry.
The plugin's package.json exports Client and Server entries, which the App registers in its two plugins.ts files. The dependency makes the plugin resolvable; registration loads its capabilities. See Create a plugin for generator usage and the complete tree below for other capabilities.
Complete directory structure
The demo-full example below summarizes the complete capability layout for a local plugin inside an App. The scaffold generates client/, server/, database/, cli/ and skills/ according to the selected capabilities; keep the parts your plugin needs. The tree also includes client/pages/, added as needed during development, and dist/, generated by the build. Only additional locale filenames are omitted.
packages/app-plugin-demo-full/
├── client/ # Client (optional)
│ ├── components/ # Plugin components
│ │ ├── plugin-component.tsx
│ │ └── provider.tsx
│ ├── contexts.ts # React context definitions
│ ├── index.ts # Public Client entry
│ ├── locales/ # Client translations
│ │ ├── en-US.ts
│ │ ├── index.ts
│ │ ├── zh-CN.ts
│ │ └── …
│ ├── pages/ # Added as needed: business pages
│ ├── plugin.ts # Compose Client contributions
│ ├── providers/ # Client services and lifecycle
│ │ ├── demo-full.ts
│ │ └── index.ts
│ ├── react-providers/ # Register React Providers
│ │ └── index.ts
│ ├── routes.ts # Page route declarations
│ └── styles.css # Client styles
├── server/ # Server (optional)
│ ├── index.ts # Public Server entry
│ ├── jobs/ # Background jobs and executor Provider
│ │ ├── demo-full.ts
│ │ └── provider.ts
│ ├── locales/ # Server translations
│ │ ├── en-US.ts
│ │ ├── index.ts
│ │ ├── zh-CN.ts
│ │ └── …
│ ├── plugin.ts # Compose Server contributions and resource paths
│ ├── providers/ # Service registration and lifecycle
│ │ ├── demo-full.ts
│ │ └── index.ts
│ ├── routes/ # HTTP endpoint declarations
│ │ └── index.ts
│ ├── services/ # Business implementations
│ │ └── demo-full.ts
│ └── tokens.ts # Public service interfaces and tokens
├── database/ # Plugin data (optional)
│ ├── README.md
│ ├── migrations/ # Migration drafts; implement before enabling
│ │ └── 202610260001_demo_full_create_records.ts.example
│ └── seeds/ # Seed drafts; implement before enabling
│ └── 202610260002_demo_full_create_welcome_record.ts.example
├── cli/ # CLI (optional)
│ ├── index.ts # CLI declaration
│ └── info.ts # Example command implementation
├── skills/ # Application agent guidance (optional)
│ └── nocobase-app-plugin-demo-full/
│ └── SKILL.md
├── tests/ # Tests for selected capabilities
│ ├── cli/
│ │ └── info.test.ts
│ ├── client/
│ │ ├── component.test.tsx
│ │ ├── react-provider.test.tsx
│ │ ├── routes.test.ts
│ │ └── service-provider.test.ts
│ ├── database/
│ │ └── migrations.test.ts
│ └── server/
│ ├── jobs.test.ts
│ ├── plugin.test.ts
│ ├── routes.test.ts
│ └── service-provider.test.ts
├── dist/ # Generated by build: compiled plugin output
├── components.json # shadcn configuration for Client code
├── package.json # Dependencies, exports and scripts
├── package.ts # Foundation marker; keeps empty plugins buildable
├── tsconfig.json # Type checking and compilation
├── eslint.config.js # Lint configuration
├── vitest.config.ts # Test configuration
├── .gitignore # Git ignore rules
├── .prettierignore # Formatting ignore rules
├── AGENTS.md # Plugin development guidance
├── CLAUDE.md # Agent guidance referencing AGENTS.md
├── README.md # Plugin documentation
└── CHANGELOG.md # Release historyMigration and seed timestamps vary with the generation date. Use client/pages/ to organize actual pages and connect them through client/routes.ts; creating the directory does not register routes.
The tree contains three kinds of files: generated scaffold files, source added during development, and build output. client/pages/ belongs to the second category and dist/ to the third. Generated capability directories depend on your selections; see Create a plugin for options.
How entries connect
Implementation files, public plugin entries and App registration files serve different purposes:
| Location | Responsibility |
|---|---|
| Plugin components, routes, services or commands | Implement the feature |
Plugin client/plugin.ts and server/plugin.ts |
Compose contributions for each runtime, such as routes, Providers and database resources |
Plugin client/index.ts and server/index.ts |
Export the plugin definition; the scaffold normally re-exports the adjacent plugin.ts |
Plugin cli/index.ts |
Declare and export the CLI plugin directly, without a separate cli/plugin.ts |
Plugin package.json |
Declare these entries as public package exports |
App client/plugins.ts, server/plugins.ts and cli/plugins.ts |
Register the corresponding entries provided by plugins |
The announcement page and API each have an integration path:
Client: pages/announcements.tsx → client/routes.ts → client/plugin.ts
→ client/index.ts → package.json ./client → App client/plugins.ts
Server: server/routes/index.ts → server/plugin.ts
→ server/index.ts → package.json ./server → App server/plugins.tsThe plugin's client/plugin.ts and the App's client/plugins.ts belong to different packages: the former defines what the plugin contributes, while the latter selects the plugins the application loads. Adding a page or API usually changes implementation and route declarations inside the plugin. An App that already registers the runtime entry does not need to register the plugin again for each page or endpoint.
Three runtime entries
| Public plugin export | App registration file | Integration |
|---|---|---|
<package>/client |
client/plugins.ts |
Call the Client factory, optionally with supported configuration |
<package>/server |
server/plugins.ts |
Include the Server plugin definition |
<package>/cli |
cli/plugins.ts |
Include the CLI plugin definition |
Integrate only the entries the plugin provides. For example, selecting only client.components produces public component exports without client/index.ts or client/plugin.ts; the App imports those components directly. Selecting database generates a Server entry that declares its database resources.
The generator creates entries, while pnpm nocobase plugin register connects runtime entries to the App. See Write your first plugin for the complete workflow.
Key files
package.json: metadata, dependencies and public exports
The plugin maintains its own package.json, separately from the one at the App root:
| Field | Role in the plugin |
|---|---|
name, version |
Identify the plugin package and its version |
private |
Defaults to true for local plugins, which can be used inside an App without npm publication |
exports |
Declare import paths available to consumers; the scaffold uses source entries for local development |
publishConfig.exports |
Map exports to compiled artifacts; also used when the App build collects local packages |
files |
Declare package contents such as dist/, plus skills/ when selected |
scripts |
Run the plugin's checks, tests and build |
The App's package.json references the local plugin through workspace:* in dependencies. That dependency makes the package resolvable; runtime registration determines which capabilities the App loads.
Declare dependencies in the package that uses them and preserve the scaffold's categories: ordinary Server runtime dependencies belong in dependencies; runtimes that must share identity with the host, and Client runtime dependencies, belong in peerDependencies; testing and build tools belong in devDependencies. A package being installed in the App does not replace the plugin's own dependency declaration.
For the announcements plugin, the Client and Server export fields are:
{
"exports": {
"./client": {
"types": "./client/index.ts",
"import": "./client/index.ts"
},
"./server": {
"types": "./server/index.ts",
"import": "./server/index.ts"
}
},
"publishConfig": {
"exports": {
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.js"
}
}
}
}The App uses @nocobase/app-plugin-announcements/client and @nocobase/app-plugin-announcements/server rather than private source paths. Keep source and compiled mappings aligned when adding public components, services or types.
database: plugin data resources
database/migrations/ holds migrations and database/seeds/ holds seed data. Generated .ts.example files are drafts and do not execute. Complete them before removing the .example suffix, and keep each filename aligned with its exported task name.
server/plugin.ts declares these directories through its database configuration. Resource paths resolve relative to the plugin's absolute baseDir:
| Loaded code | baseDir points to |
Database resource locations |
|---|---|---|
Source server/plugin.ts |
Plugin root | database/migrations/, database/seeds/ |
Compiled dist/server/plugin.js |
Plugin dist/ |
dist/database/migrations/, dist/database/seeds/ |
Keep the generated path calculation and database build script. A plugin with database capabilities generates .manifest.json files after compilation; preserve them with the compiled migrations and seeds. Building prepares resources without running migrations.
UI components and source ownership
client/components/ contains plugin-maintained components. Add client/pages/ as needed for business pages and connect them through client/routes.ts. Public components are exposed through package exports.
Plugins with Client code include components.json and client/styles.css to configure shadcn file locations, package-local aliases and the style entry. Both Apps and plugins can install components or blocks from the UI Library. The App maintains source installed into the App; the plugin maintains source installed into the plugin. When consumers use a component through a plugin's public export, the plugin continues to maintain its implementation.
Aliases such as #components/* are defined by the plugin's own package.json, independently of private App aliases. When adding or installing components, keep file locations, imports and build configuration aligned.
AGENTS.md and skills/: two kinds of agent guidance
| File | Audience | Purpose |
|---|---|---|
Plugin root AGENTS.md |
Developers and agents maintaining plugin source | Internal development conventions |
Plugin root CLAUDE.md |
Claude Code working in the plugin | Reference the same AGENTS.md |
Plugin skills/*/SKILL.md |
Application agents consuming the plugin | Explain capabilities, integration and verification |
App .agents/skills/ |
Agents developing the application | Hold synchronized guidance copies |
Plugin development guidance is part of the foundation; skills/ is generated when selected. Complete the Skill source in the plugin before synchronizing it, rather than maintaining generated App copies directly.
Tests and development configuration
tests/ organizes tests by capability. tsconfig.json, eslint.config.js and the conditionally generated vitest.config.ts configure types and compilation, linting and test execution. When adding a capability, update its implementation, declarations, public exports, dependencies and tests together. See Write your first plugin for check commands.
Source, plugin builds and App builds
The plugin's dist/ and the App's dist/ are separate levels of build output:
| Location | Contents |
|---|---|
packages/app-plugin-announcements/client/, packages/app-plugin-announcements/server/ |
Plugin Client and Server source |
packages/app-plugin-announcements/dist/ |
Compiled plugin JavaScript and type declarations |
dist/ at the App root |
Application build output, including application code and runtime dependencies |
Build the plugin on its own from the App root:
pnpm --filter @nocobase/app-plugin-announcements buildDuring development, the App uses the local plugin through workspace dependencies and source exports. To prepare the application for delivery, run from the App root:
pnpm buildThe App build builds local packages in dependency order and collects the workspace packages needed for delivery under the application's dist/vendor/, using compiled export mappings and local dependency paths. A local plugin can ship with the App without being published to npm first.
These artifacts are generated. Change source files and rebuild rather than editing dist/ directly. After the plugin builds successfully, verify its actual use in the App and the complete application build.
Next step
Follow Write your first plugin to implement the announcements API and page, then test, register and verify them in the App. For capability options and combinations, see Create a plugin. For installation, updates and removal, see Manage plugins.
For sharing across Apps with independently managed versions, continue with Publish a plugin. Publication is unnecessary when using the plugin only in the current App.
