Staged publishing
Staged publishing puts a review step in front of every publish. A version is uploaded to the registry but stays invisible to installs until a maintainer looks at it and approves it. If nobody approves it, it never becomes a real version.
It is driven entirely by npm stage, so no extra tooling is needed on the
client side.
This feature ships in Verdaccio 7.x and later. It is not available in 6.x, which does not receive new features.
To try it today, use the 9.x experimental line, which is where it lands first:
npm install -g verdaccio@next-9
# or
docker pull verdaccio/verdaccio:nightly-master
Experimental releases are not production-ready and are meant for testing and
early feedback. Node.js >= 24 is required.
This is an experimental feature behind the stage flag. It is off by default,
and the shape of the flag or its behaviour may change in a future release. See
feature flags.
Not recommended for production yet. We would rather gather enough real usage first — see below on how to help with that.
This feature is experimental and shaped by what people report while using it. Tell us whether the workflow fits how your team releases, what it does not cover for your setup, and any rough edge or confusing error — in GitHub Discussions, as an issue at verdaccio/verdaccio, or on Discord.
Reports about what does not work are the most useful thing you can send while a feature is still experimental: they decide whether it graduates, changes shape, or gets dropped.
Requirements
- npm 11.17 or newer on the client.
npm stagedoes not exist in older versions, and there is no Yarn or pnpm equivalent yet. - A storage plugin, which is anything that works with Verdaccio today. Staged versions go through the same storage interface as everything else.
Enabling it
flags:
stage: true
Restart the registry afterwards: the configuration is read at startup.
With the flag off, the npm stage commands answer 404 and the registry behaves
exactly as it always has.
The flow
npm stage publish the version is uploaded but not installable
npm stage list maintainers see what is waiting
npm stage view <id> inspect the metadata
npm stage download <id> inspect the actual tarball
│
├── npm stage approve <id> published for real, installable now
└── npm stage reject <id> record and tarball deleted, never existed
Staging a version
From the package directory, exactly where you would run npm publish:
npm stage publish
npm notice Staging to http://localhost:4873/ with tag latest and default access
+ my-package@1.0.0 (staged with id 9efe8b0c-50b3-4183-ba3f-c46dfc784adb)
--tag works as usual, so npm stage publish --tag beta stages the version to
be published under beta once approved.
At this point the version does not exist as far as installs are concerned:
npm view my-package
# npm error 404 'my-package' is not in this registry
Reviewing what is waiting
npm stage list
id: 9efe8b0c-50b3-4183-ba3f-c46dfc784adb
package name: my-package
version: 1.0.0
tag: latest
date staged: 2026-08-29T14:21:45.777Z
staged by: alice (user)
shasum: 54ec43441cacc23ad02ecec43ab46037b0e98aeb
access: public
npm stage list <package-name> narrows the list to one package, and
--json gives you the raw records, which is what you want in a script:
npm stage list --json | jq -r '.[0].id'
The list only shows what you are allowed to see — see who can do what.
Inspecting before approving
npm stage view <id> repeats the metadata for a single item. The interesting
one is:
npm stage download <id>
This downloads the staged tarball and prints its contents, so you can check what is actually in the package before it becomes installable:
npm notice Tarball Contents
npm notice 24B index.js
npm notice 274B package.json
npm notice Tarball Details
npm notice name: my-package
npm notice shasum: 54ec43441cacc23ad02ecec43ab46037b0e98aeb
my-package-1.0.0-9efe8b0c-50b3-4183-ba3f-c46dfc784adb.tgz
The shasum matches the one npm computed when it packed the tarball, so you can
verify nothing changed in transit.
Approving
npm stage approve 9efe8b0c-50b3-4183-ba3f-c46dfc784adb
Staged package 9efe8b0c-50b3-4183-ba3f-c46dfc784adb approved and published successfully.
Approval replays the ordinary publish path, so everything that normally happens on a publish happens here too: the version is checked against upstreams, dist-tags are merged, filter plugins run, and notifications fire. The staged copy is removed once the version is really published.
The version is installable immediately afterwards.
Rejecting
npm stage reject 9efe8b0c-50b3-4183-ba3f-c46dfc784adb
The staged record and its tarball are deleted. The version never existed, so the
same name@version can be staged again later.
Who can do what
Staging has its own entry in package access, alongside access,
publish and unpublish:
packages:
'my-company-*':
access: $authenticated
stage: developers
publish: release-managers
| Action | Who |
|---|---|
npm stage publish | anyone allowed to stage that package |
npm stage list | items you staged, plus items on packages you may publish |
npm stage view / download | the person who staged it, or anyone who may publish it |
npm stage approve | anyone allowed to publish that package |
npm stage reject | the person who staged it, or anyone who may publish it |
Asking about a staged item you are not allowed to see answers 404, not 403.
A 403 would confirm the id exists to someone who has no business knowing that.
The fallback
stage is optional. When a package entry does not mention it, whoever may
publish may also stage — exactly the behaviour before the permission existed —
so no existing configuration changes meaning. This mirrors how unpublish
already falls back to publish.
Making review a real gate
stage only becomes interesting when it is granted to a group that does not
have publish:
packages:
'my-company-*':
access: $authenticated
stage: developers # may propose a release
publish: release-managers # may actually make one
With that configuration a developer can run npm stage publish but cannot
npm publish, and cannot approve their own submission either. A release manager
reviews the tarball and approves it. That is a genuine four-eyes workflow rather
than a convention people can bypass.
Without the split — that is, with stage unset — the same person can stage and
approve their own version. Staging is then a convenience, not a control.
Note that a developer can always withdraw their own submission with
npm stage reject, even without publish rights. Retracting your own proposal is
not the same as rejecting somebody else's.
The web interface
When the flag is on, logged-in users get a Staged packages entry in the
account menu, or you can go straight to /-/web/stage.
The list shows the same fields as the CLI, and each row has buttons to download the tarball, approve or reject. Approving and rejecting both ask for confirmation first, because neither can be undone from the UI. Clicking the package name opens a detail view with the shasum and access level.
You must be logged in: the page needs your session to call the registry.
Interaction with two-factor authentication
Staged publishing and two-factor authentication are independent — you can run either on its own. When both are on, they compose the way npmjs intends:
npm stage publishnever asks for a one-time password. Being able to publish without proving presence is the point of staging: a CI job can stage a release and a human approves it later.npm stage approveandnpm stage rejectdo ask for one, if the maintainer hasauth-and-writesmode.
That combination is the reason npm stage exists on npmjs, and it is what lets
an automated pipeline prepare a release without holding a second factor.
Notifications
If you have notifications configured, staging fires one with
publishType: stage, and rejecting fires publishType: unstage. Approving fires
publish, like any other publish.
That is what makes the review step workable in practice — otherwise a staged
version waits until somebody happens to run npm stage list:
notify:
method: POST
endpoint: https://hooks.example.org/reviews
content: '{"type":"{{ publishType }}","package":"{{ publishedPackage }}","by":"{{ publisher.name }}"}'
See publish type for the full list of values.
Where staged versions are stored
Staged items live under a reserved namespace in whatever storage backend you already use:
<storage>/.stage/package.json index of everything staged
<storage>/.stage/<uuid>/package.json the staged record and its metadata
<storage>/.stage/<uuid>/<name>-<v>.tgz the staged tarball
The namespace is deliberately never registered in the storage plugin's package
database, so staged versions never show up in search, in /-/all, or on the web
UI home page.
Limitations
Worth knowing before you turn this on:
- Nothing expires. A staged version that nobody approves or rejects stays on
disk forever. There is no automatic cleanup yet, so keep an eye on
<storage>/.stageif staging is used heavily. - One Verdaccio process. The index of staged items is kept consistent within a process. Several Verdaccio instances sharing one storage backend are not supported for this feature.
- Approval holds the tarball in memory. The tarball is read back in full to be republished, so approving a very large package uses memory proportional to its size.
- Staging and approving are the same permission. See who can do what.
- npm only. Yarn and pnpm have no equivalent command.
故障排除
404 Not Found - POST http://localhost:4873/-/stage/package/...
The flag is off, or the server was not restarted after enabling it. Check that the running server is using the configuration file you edited.
npm error code E404 on npm stage list
Same cause. With the flag off none of the routes are registered.
A version was approved but npm install cannot find it
Check whether you have min-release-age set in your .npmrc. npm refuses to
install packages published more recently than that, which looks like the
registry losing the version:
npm install my-package --min-release-age=0
409 Conflict when staging
Either that exact name@version is already published, or it is already staged.
npm stage list <package> shows the latter.
409 Conflict when approving
The version was published by some other route while it sat in the queue. The staged item is kept on purpose so you can still inspect it and decide; reject it if the published version is the one you wanted.