First package? Follow the publish and install quickstart →
Watch lesson 2: Scopes and registry routes (2 minutes)Check your setup locally with the registry auth checker. For old tarball destinations, use the lockfile registry inspector.
Overview
This guide shows how to configure npm, pnpm and Yarn to install private packages from PrivateNPM. You add a route for your scope and a credential for the registry host to a configuration file. The result is a project that installs your private packages from your registry and public packages from npmjs.org.
The guide is for developers, CI maintainers and recipients who received a download key. The examples use the shared registry at registry.privatenpm.com, the scope @acme, the package @acme/sdk and the environment variable ACME_NPM_TOKEN. Replace these values with your values. If npm already returns E401, use the npm E401 checklist.
Before you start
Make sure that you have these items:
- A download key (
pdk_…) for installs, or a publisher key (prt_…) for releases. - The registry URL. For the shared registry, the URL is
https://registry.privatenpm.com/. - For a private registry, its active custom domain, for example
https://npm.acme.com/. - The scope of the packages that you install, for example
@acme. - npm 11 and Node.js 24. The pnpm and Yarn sections give their own conditions.
A member of the publisher organization issues keys. Open Registry → Registries and click Manage keys on the registry card. A publisher key needs the registry:credential:manage permission, and a download key needs registry:download_key:manage. The built-in Admin role has both permissions. A recipient receives a download key from the publisher or from the recipient download portal.
How it works
Where npm reads its configuration
npm merges configuration from several sources. A value from a source that is higher in this list replaces a value from a source that is lower in the list:
- Command-line flags, such as
--registry. - Environment variables with the prefix
npm_config_, such asnpm_config_registry. - The project
.npmrcfile, in the same directory aspackage.json. - The user file,
~/.npmrc. Theuserconfigoption can select a different file. - The global file,
$PREFIX/etc/npmrc. - The built-in defaults of npm.
A recommended pattern combines a committed project file that contains no secret with a credential in your user file or in the environment. Run each diagnostic command in the same directory and environment as the command that failed. A different directory can select a different project file. The npmrc documentation gives the full rules.
The two lines that a private scope needs
@acme:registry=https://registry.privatenpm.com/
//registry.privatenpm.com/:_authToken=${ACME_NPM_TOKEN}The first line is the route. It tells npm to send every request for a package in the @acme scope to the private registry. Packages without that scope continue to come from the default registry. The default registry is https://registry.npmjs.org/ unless you change it.
The second line is the credential. The line starts with //, has no protocol, and ends with :_authToken=. The host in the credential line must be the same as the host in the registry URL. The npm CLI replaces ${ACME_NPM_TOKEN} with the value of the variable when it reads the file. Thus a committed file does not contain the secret.
For a private registry on a verified custom domain, use the custom domain in both lines after the domain is active:
@acme:registry=https://npm.acme.com/
//npm.acme.com/:_authToken=${ACME_NPM_TOKEN}The requests that npm sends
An install of @acme/sdk causes two types of HTTP request. First, npm requests the package document, which npm calls the packument. The URL is https://registry.privatenpm.com/@acme%2fsdk. The packument lists each version, the dist-tags and the tarball URL of each version. For an install, npm asks for the short form of the packument with the Accept: application/vnd.npm.install-v1+json header.
Second, npm downloads the tarball of the version that it selected. On PrivateNPM, the tarball URL is on the host that served the packument, for example https://registry.privatenpm.com/@acme/sdk/-/sdk-1.2.0.tgz. The registry authorizes each request again. The packument needs metadata access, and the tarball needs download access to that exact version.
The npm CLI sends the credential in the Authorization: Bearer header. The CLI selects the credential by URL. It compares the request URL with the host and path of each :_authToken line. If no line matches, npm sends the request without a credential. Thus a correct key on a line for the wrong host has the same result as no key.
The registry does not accept anonymous requests. Each request for a packument, a version or a tarball must carry a valid publisher key or download key.
What the registry returns
| Status | Result on PrivateNPM | First check |
|---|---|---|
| 401 | The request had no credential, or the registry did not accept the credential. Unknown, expired and revoked keys return 401. A key of a suspended recipient also returns 401. | The host of the credential line and the value of the variable. |
| 403 | The registry accepted the credential, but the key does not permit the operation. | The package access, the actions and the registry of the key. |
| 404 | The package does not exist in this registry, or a different organization owns it. | The scope route, the package name and the registry. |
The body of a 403 response contains a reason code. For example, a download key that does not grant the package causes the code not_entitled. The E401 guide explains each reason code.
Configure your user ~/.npmrc
Use the user file on your own computer. Each project on the computer can use the credential, and no secret goes into a repository. Before you start, you must have the value of the key.
- Add the scope route to your user file:
npm config set @acme:registry https://registry.privatenpm.com/ --location=user - Add the credential line to your user file:
npm config set //registry.privatenpm.com/:_authToken=pdk_... --location=user - Show the route that npm uses:
npm printsnpm config get @acme:registryhttps://registry.privatenpm.com/. - Request the packument:
npm prints the latest version, for examplenpm view @acme/sdk version1.2.0.
The --location=user flag writes ~/.npmrc. The --location=project flag writes the project file. After you create a key, the dashboard shows the same lines under Use the key in your user .npmrc.
WARNING: Do not put the value of a key in a project file or in a dotfiles repository. Anyone who can read the file can install your packages, or publish versions with a publisher key.
Commit a project .npmrc with an environment variable
Use this form for a project that other people or pipelines build. The file contains the route and a reference to a variable. The file does not contain the secret.
@acme:registry=https://registry.privatenpm.com/
//registry.privatenpm.com/:_authToken=${ACME_NPM_TOKEN}Before you start, you must have a download key and access to the shell or the CI secret store of the build.
- Save the file as
.npmrcin the directory ofpackage.json, and commit it. - Set the variable in the shell or in the CI secret store:
export ACME_NPM_TOKEN=pdk_... - Make sure that the variable is present. This command does not print the value:
The command printsnode -e 'if (!process.env.ACME_NPM_TOKEN) { console.error("ACME_NPM_TOKEN is missing or empty"); process.exit(1); } console.log("ACME_NPM_TOKEN is set");'ACME_NPM_TOKEN is set. - Install the dependencies:
npm installs the private packages and the public dependencies.npm ci
npm does not read .env files. The variable must be in the environment of the npm process. A tool such as direnv, or a script that exports the variable, can supply it. For one command only, put the variable before the command:
ACME_NPM_TOKEN=pdk_... npm ciNOTE: If the variable is not set, npm does not send a usable credential. The registry then returns 401. The check in step 3 finds this problem before the install starts.
pnpm does not expand this reference in a project file. For pnpm, use the pnpm section.
Send all packages to the private registry
A scope route sends only one scope to the private registry. Two cases need the default registry instead. The first case is an unscoped private package on a private registry. The second case is public npm caching through your registry.
registry=https://npm.acme.com/
//npm.acme.com/:_authToken=${ACME_NPM_TOKEN}With this file, npm sends every request to npm.acme.com, also the requests for public packages. Use this file only in projects that need it. For one command, use --registry=https://npm.acme.com/ instead. When you move a registry to a different host, change the registry URL and the credential host together.
Public npm caching is available on the Growth plan and higher plans. Enable it on the Public packages page of the registry. Also enable Allow this key to install public packages through the cache on the download key. If either option is off, public names return 404.
The registry never fetches a name that you host, or a name in a scope that an organization claimed on that registry. If the registry cannot classify a name, it returns 502 and does not use the public registry. The public cache guide gives the full configuration.
Configure GitHub Actions and other CI
With actions/setup-node, the registry and the scope are inputs. The action writes a temporary .npmrc file that reads the credential from NODE_AUTH_TOKEN. Supply that variable from your secret store on the install step:
- uses: actions/setup-node@v7
with:
node-version: '24'
registry-url: 'https://registry.privatenpm.com/'
scope: '@acme'
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.ACME_NPM_TOKEN }}Attach the secret only to the step that installs dependencies. By default, GitHub does not give repository secrets to a workflow that a pull request from a fork starts. Thus a build that passes on a branch can fail with E401 on a pull request from a fork.
Other CI systems
In other CI systems, write the credential line during the build. Use the CI secret store for the value, and keep the committed route lines:
echo "//registry.privatenpm.com/:_authToken=$ACME_NPM_TOKEN" >> .npmrc
npm ciMark the variable as protected or masked, so that the CI log does not show it. Use a download key that grants only the packages that the build needs.
WARNING: Do not commit a .npmrc file that a build changed. The file contains the value of the key.
Install private npm packages in CircleCI
Before you start, commit the project .npmrc from the environment-variable procedure and a package lockfile.
- Create a read-only download key for the required packages.
- In CircleCI, store the key as
ACME_NPM_TOKENin a restricted context. - Replace
private-package-downloadsbelow with the name of that context. - Add this job to
.circleci/config.yml:version: 2.1 jobs: install: docker: - image: cimg/node:24.21.0 steps: - checkout - run: name: Install dependencies command: npm ci --ignore-scripts workflows: verify: jobs: - install: context: private-package-downloads - Run the workflow on a trusted branch. The install must complete without E401.
WARNING: Do not expose the context to untrusted fork builds. Code in a job can read its secrets.
Use a separate publisher key for a release job. PrivateNPM supports OIDC publishers from GitHub Actions and GitLab CI.
CircleCI OIDC instructions for npmjs.org do not configure a PrivateNPM trusted publisher. See CircleCI token configuration.
Configure Vercel
Add ACME_NPM_TOKEN as an environment variable on the Vercel project. Select the environments that install the private packages. Vercel gives the variable to the build step that runs npm install. Thus the committed project .npmrc with the ${ACME_NPM_TOKEN} reference is the full configuration. To rotate the key, replace the variable and deploy again. The committed file does not change.
Configure pnpm
pnpm 12 reads the scope route from the project .npmrc. Since pnpm 11.5.3, pnpm does not expand environment variables in the authentication values of a project file. Keep the route in the project file, and give the credential to each command through the environment:
env "pnpm_config_//registry.privatenpm.com/:_authToken=$ACME_NPM_TOKEN" pnpm install --frozen-lockfile --ignore-scriptsThe variable name contains the host of the credential, as in the .npmrc line. You can also keep the reference in your user-level authentication file. See the pnpm authentication rules. The command above refers to a variable that your secret store sets. Thus the secret does not go into the shell history.
Configure Yarn
Current Yarn releases ignore .npmrc. They read .yarnrc.yml instead:
npmScopes:
acme:
npmRegistryServer: "https://registry.privatenpm.com/"
npmAuthToken: "${ACME_NPM_TOKEN}"The key under npmScopes is the scope name without the @ character. Yarn reads ACME_NPM_TOKEN from the environment. The yarnrc reference gives the other registry options.
Fix “npm warn Unknown user config always-auth”
npm 11 does not support the always-auth option. Since npm 11.2, npm writes a message for each unknown configuration key. Old setup snippets can leave this line in a file. The setup-node issue records one source of the message.
If npm reports Unknown user config "always-auth" or always-auth is not a valid npm option, find the file or the command that adds the line. These commands show the paths of the files. They do not show credentials:
npm config get userconfig
npm config get globalconfig- Open the file that contains the
always-authline. For an unknown project option, open the project.npmrc. - Remove only the
always-auth=...line. Keep the scope route and the:_authTokenline. - If CI writes the file, remove the
always-authline from the step that writes it. - Run the command again. npm does not show the message.
The removal of an unsupported option does not supply a key. If the command still returns 401, continue with the E401 guide. Before you change a file that a different package manager also reads, examine the documentation of that client.
Verify the result
Test the configuration in a clean environment: a new directory, an empty npm cache and only the configuration of the build. A local cache can supply a package without a request to the registry. Before you start, the variable ACME_NPM_TOKEN must be set in the shell.
- Create an empty directory and copy the project
.npmrcinto it. - Make sure that the registry answers:
npm prints a line that containsnpm ping --registry=https://registry.privatenpm.com/PONG. This result proves only the network path. - Make sure that the registry accepts the credential:
For a download key, npm printsnpm whoami --registry=https://registry.privatenpm.com/download:and the key ID. For a publisher key, npm prints the email address of the member. - Request the packument:
npm prints the latest version.npm view @acme/sdk version - Install the package with an empty cache:
npm prints a line similar tonpm install @acme/sdk --no-save --cache "$(mktemp -d)"added 1 package.
Each step tests one more part of the request path. If step 4 passes and step 5 fails with 403, the key can read metadata but cannot download tarballs. Examine the actions of the key, for example Read tarballs on a publisher key.
Troubleshoot the install
| Symptom | Cause | Correction |
|---|---|---|
| 401 Unauthorized on each request | No credential line matches the request URL, or the variable is not set. | Make the credential host the same as the registry host. Include the port if the URL has one. Do the check in step 3 of the environment procedure. |
| 401 after the key worked before | Someone revoked or rotated the key, the key expired, or the publisher suspended its recipient. | Obtain the current key value and update the secret store. |
| 403 Forbidden | The key is valid but does not permit the operation. | Examine the package access, the actions and the registry of the key. Read the reason code in the response. |
| 404 for a private package | The scope route is absent, so the request went to npmjs.org. The name can also be wrong. | Run npm config get @acme:registry and compare the package name with the dashboard. |
| 404 for a public package through the default registry | Public npm caching is off for the registry or for the key. | Enable both options, or remove the default registry line. |
| 502 Bad Gateway for a public name | The registry cannot classify the name. It fails closed. | Run the command again later. The registry does not use the public registry as a fallback. |
| 401 with pnpm and a correct project file | pnpm ignores the environment reference in the project file. | Use the method in the pnpm section. |
| Metadata works but a tarball fails | The lockfile records tarball URLs on an old host. | Examine the resolved URLs in the lockfile. Regenerate the lockfile after a change of host. |
For a step-by-step diagnosis, use Fix npm E401: private registry authentication.
Security and maintenance
- Give each person, pipeline and recipient a separate key. Then you can revoke one key and keep the others.
- Use a download key for installs. A download key is read-only and cannot publish.
- Grant only the packages that a build or a recipient needs.
- Set an expiry date on a key for temporary access. The key stops at the start of that day.
- Rotate a key after a staff change or a possible leak. Use Rotate key on the page of the key.
- After a rotation, the old secret stops. Put the new secret in each secret store that used the old one.
- Revoke a key that nobody needs. Use Revoke key on the page of the key.
- Examine the downloads of each key under Registry → Activity. The Access denied tab lists refused requests with a reason.
The npm config list command shows (protected) in place of each _authToken value. Use that command when you share configuration in a support request. Do not paste a full .npmrc file or a full environment into an issue.
Related guides
- Fix npm E401: private registry authentication
- Publish a private npm package
- npm trusted publishing with OIDC
- Verify a custom npm registry domain
- Recipient npm download portal
- Cache public npm packages safely
Next: Automate CI
Publish new versions from CI with OIDC and no stored key.
You need an organization to do these steps. Start a 30-day trial. Before you add a card, you can publish 3 packages and store 10 GB of content.