Integrate the Users plugin into a NocoBase App, build a user management page on its API and configure its permissions, or add an application-owned role scope through the public Server contracts.
Installs into .claude/skills of the current project.
Are you the author of Nocobase App Plugin Users?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nocobase-nocobase-app-plugin-users)
---
name: nocobase-app-plugin-users
description: Integrate the Users plugin into a NocoBase App, build a user management page on its API and configure its permissions, or add an application-owned role scope through the public Server contracts.
metadata:
short-description: Integrate reusable user administration
---
# User Management App Plugin
Use this Skill when an application needs user management, or when another
plugin needs to expose application-specific roles in user management. Do
not use it to modify the Users plugin source or to replace Authentication's
user, account, or Session storage.
## Public surfaces
- Client registration factory and `UsersClientOptions`:
`@nocobase/app-plugin-users/client`. It contributes only the public
invitation page at `/invite/:token`; there is no user management page.
- Typed API client: `UsersClient` and its types from
`@nocobase/app-plugin-users/client/user-client`.
- Server contracts: `userManagementServiceToken`,
`userRoleScopeRegistryToken`, `UserRoleScope`, and related types from
`@nocobase/app-plugin-users/server/tokens`.
- HTTP API: `GET /api/users` (`page`, `pageSize`, `q`, `status`, `roleScope`, `role`, `sort=name`, `order=asc|desc`; answers `{ data, meta: { page, pageSize, total } }`), `GET /api/users/options`, `GET /api/users/:userId` (latest managed profile and role scopes, requires `user/read`, 404 for absent accounts), `POST /api/users`, `PATCH /api/users/:userId`, `DELETE /api/users/:userId?confirm=true`, `POST /api/users/:userId/disable|enable|resetPassword|revokeSessions`, and `PUT /api/users/:userId/roleScopes/:scope`. The running application documents each one, with request and response schemas, at `/api/swagger/docs` (JSON at `/api/swagger`) under the `Users` tag. Failures use the standard error body; branch on `error.reason`, such as `USER_NOT_FOUND`, `SELF_DELETE_NOT_ALLOWED`, `LAST_ASSIGNMENT` or `USER_EMAIL_CONFLICT`.
`UsersClient.update(userId, { name?, username?, email?, roleScopes? })` supports one atomic editor Save. Profile fields require `update`; submitted `roleScopes` additionally require `assign-role`. A roles-only payload requires `assign-role` without `update`. The service validates scope values, runs each applicable target-account guard, and commits profile changes and all submitted scope replacements in the caller-owned transaction, notifying permission clients only after commit. Omitted scopes are preserved. Keep the existing per-scope PUT for callers needing only one scope. Do not implement a combined Save as sequential profile and role requests: a later failure would leave a partially saved account.
Every HTTP route requires Authentication and Authorization. Routes check the `user` record type with the applicable action from `read`, `create`, `update`, `disable`, `enable`, `assign-role`, `reset-password`, `revoke-sessions` or `delete`. Account creation checks both `create` and `assign-role`. `delete` is permitted only while an application role scope can clean a deleted user up.
## Register the plugin
1. Register Authentication, Authorization, and then Users in the App's Client
and Server plugin arrays. The Client factory takes no placement options:
`users()`.
2. Grant only the `user` actions those roles need, on `{ type: 'user', id: '*' }` because `user` is a record type. The plugin creates no roles and grants no access by itself.
3. For a user management page, build it in the App on `UsersClient` and declare it among the App's own routes with the page access it needs; the plugin ships none.
The invitation page reports results through `useToaster()` from `@nocobase/app-client`, so the App needs the `@nocobase/app-client` that exports it and registers a toaster service, as the templates do: `client/lib/toaster.ts` from the template, and `this.app.container.instance(toasterToken, createToaster())` in the `register()` of `client/service-provider.ts`, with the `Toaster` component mounted in `client/react-providers.ts`. Without the registration nothing throws, but its toasts are only logged to the browser console.
The default `app` permission-set scope is supplied by Users whenever the Authorization plugin's `authorizationToken` is available. Do not copy a user-roles Provider into an application. Set `users.permissionSets: false` to replace the default with an application-owned scope.
## Add an application role scope
Resolve `userRoleScopeRegistryToken` in an application or business plugin
ServiceProvider and register one `UserRoleScope` during `boot()`. The scope owns
its available options, current assignment lookup, role filtering, atomic
replacement, and any disable guard. Unregister it during `shutdown()`.
Use the `DatabaseConnection` passed to each scope method. This is the
caller-owned transaction used to keep account creation or state changes atomic
with role assignments. Do not open an unrelated transaction and do not write
Authentication's internal tables.
Guard who may act on whom with `assertCanAdminister(userId, actorId, action, connection)`: the management API calls it for `update`, `disable`, `enable`, `reset-password`, `revoke-sessions` and `assign-role` with the authenticated actor, in the action's transaction and before anything changes, after authorization of the action itself has passed. Throw a `UserRoleScopeError` to refuse, for example when someone who is not an application owner resets an owner's password; the route answers `FAILED_PRECONDITION` with the scope's reason. Service callers that pass no `actorId` are the application's own server-side work and are not asked. Deletion has its own `assertCanDelete(userId, actorId, connection)`.
For a required single-role scope, set `selection: 'single'` and
`requiredOnCreate: true`. Reject invalid values in the scope and enforce
business invariants such as the last-administrator rule on the server.
Set `assignable: false` or `removable: false` on an option when user management
must show a protected assignment but must not add or revoke it. Enforce the
same rule in `replace()` because these flags only control the Client. Set
`hasAuthenticatedDefaultAccess: true` when the scope lists direct assignments
but all signed-in users also inherit separately configured default access, so a
user management page can explain that distinction instead of treating the
default as a role.
Use `labelI18nKey` with `labelI18nNs` on a scope or option when its owner has
registered Client locale resources; keep `label` as the readable fallback.
Implement optional `getMany()` when assignments can be read as a batch. Users
uses it for list pages and falls back to `get()` for existing scopes.
## Invite users
Holders of `invite` on `user` can create invitations through `POST /api/users/invitations` or `UsersClient`; choosing roles also requires `assign-role`. Results include a shareable link even when email succeeds. A queued invitation closed or rotated by another request returns `emailSent=false` without a link, while the other recipients still complete. Original inviters can renew with `sendEmail=false`, which invalidates the old link. Plugin-specific invitations must use their domain endpoint, which rechecks its own grants. Other managers may resend email but receive no link. Tokens last seven days; only hashes are stored.
New accounts register directly from the invitation link with a name and password; acceptance leaves `emailVerified=false`. Email delivery is optional: authorized original inviters can privately forward the link when the email channel is absent or fails. Links use `app.publicOrigin` when configured, otherwise the invitation request origin. Existing accounts must sign in with the invited email. Acceptance consumes only the supplied invitation and applies only its grants; handlers run in its transaction and roll back account creation on failure.
## Ownership
- Authentication owns user identity, credentials, account state, password
hashing, and Sessions.
- Authorization owns Permission Sets, grants, and assignments.
- Users owns the management API, the invitation page, orchestration transaction,
`user` authorization handler, and role-scope registry.
- The App or business plugin owns role definitions, role grants, assignments,
the user management page, and role-specific invariants.
- The plugin's `skills/` source is authoritative. `.agents/skills/` is a
synchronized copy and must not be edited.
## Permissions and constraints
- Browser route access is only navigation control. The Server independently
authenticates and authorizes every request.
- A conditional grant is not accepted as an unrestricted user-management
grant; use explicit static grants for this resource.
- Disabled users are rejected by Authentication and lose their existing HTTP
Sessions and Realtime connections.
- Password hashes, Session tokens, reset tokens, and submitted passwords are
never returned by the service or included in security events.
## Verification
- Anonymous API requests return `401`; authenticated requests without the
requested `user` action return `403`.
- Creating a user with a required role scope creates both records, while role
assignment failure rolls back the user.
- Duplicate emails or usernames return a stable `409` conflict. Password reset
and database Session revocation commit or roll back together.
- Disabling a user invalidates HTTP Sessions and Realtime connections; enabling
the user requires a new login.
- Role changes become visible after transaction commit and do not alter
assignments outside the registered scope.
- The target App passes its relevant tests, typecheck, and build. Skill
synchronization alone proves only that the copy matches this source.
Deletion uses `DELETE /api/users/:userId?confirm=true` and `user/delete` authorization. Obtain an explicit user deletion request before calling it. Application role scopes can guard deletion and clean dependent credentials transactionally. Historical user identities are retained but cannot sign in or appear in management lists.
Invitation emails use `notificationService.sendTransient()` so credentials never enter notification message snapshots or job storage. This performs one bounded attempt without durable retries or deduplication. Failures retain safe status/category diagnostics without raw provider messages. The inviter can generate and privately deliver a new link if delivery fails. Historical notification snapshots are not rewritten.
## Creation defaults and existing applications
Apply an option scope’s `defaultValue` only when creating a new account. An explicit empty array takes precedence; editing uses saved roles and never reapplies defaults. Member is a visible direct role that can be removed. A roleless person retains authenticated account basics but no role-granted business access. Upgrade Authentication, Authorization, and Users together, stop old writers before applying database tasks, and merge the template user pages and translations separately: upgrading plugins does not replace application-owned source. The new Authorization seed preserves existing users before removing Member’s global assignment; never recreate that assignment in application seeds or startup reconciliation.
## Invitation email
Prefer application `mailers`, optionally selected by `users.invitations.mailer`. Selection is explicit mailer, explicit legacy emailChannel, mailers.default, then implicit system-email. The new path needs no Notification installation; legacy sendTransient retains no-record/no-retry behavior. Users stops new invitation work, closes its own sender and waits for sends in parallel; it waits through invitation bookkeeping before the database closes. A timed-out invitation retains its real in-flight slot and is never automatically resent. The outer application shutdown budget remains `APP_SHUTDOWN_TIMEOUT_MS`. Declare mailers with `defineMailersConfig` and explicit `mailerEnvironment` mappings from `@nocobase/app-server/mailers/config`; restart after changing senders. Existing invitation permissions, tokens, share links and manual resend remain unchanged.
## User language preferences
The users Client plugin automatically integrates with `useAppLocale()` through the optional `AppLocalePreferenceContext` from `@nocobase/app-client`. Explicit selections store a JSON string at `i18n.locale` in `userPreferences`. Restore a validated server value before browser cache; if absent or unsupported, use the application default without saving it. Do not migrate unowned legacy browser locales into an account. Anonymous choices remain separate, and identity transitions invalidate old locale operations, including when Authorization remounts providers.
For recipient-language jobs, import `resolveUserLocale` and `userPreferencesServiceToken` from `@nocobase/app-plugin-users/server` and `i18nToken` from `@nocobase/app-server/i18n`. Call `resolveUserLocale(container.resolve(userPreferencesServiceToken), container.resolve(i18nToken), userId)`: a supported string preference wins, then a supported application default, then `en-US`. Load that locale with `ensureLocaleLoaded` before calling `getFixedT(namespace, locale)`. Do not derive durable language from a request session or `Accept-Language`; request negotiation remains session `locale`, header, then default. The package README contains the complete example and cache compatibility notes.