Skip to content

What is a private npm registry?

A private npm registry stores your packages and checks a credential on each request. This guide explains the requests that npm sends and the checks that the registry does.

Watch the video: npm and registries Watch lesson 1: npm and registries (2 minutes)
Lesson 1 of 10. The video has English captions. Read the transcript.

Overview

This guide explains what a private npm registry is and how npm communicates with it. It is for developers, release engineers and product owners who must give JavaScript packages to a limited group of people.

An npm registry is an HTTP server that stores packages and answers the requests of package managers such as npm, pnpm and Yarn. A private registry adds two controls to each request. Authentication identifies the credential that sent the request. Authorization decides what that credential can do. A person who knows only the package name cannot read its metadata or download its contents.

The guide describes the requests that npm sends and how npm selects a registry for each package. It also describes how npm attaches a credential to a request and what the registry checks before it answers. Then it gives a rollout procedure, a test, a table of usual failures and a security checklist.

Before you start

To do the examples in this guide, you must have these items:

  • Node.js with a current npm CLI. The examples use npm 10 or 11. pnpm and Yarn use the same registry protocol.
  • A PrivateNPM.com organization. If you do not have one, create an organization. A new organization starts with a trial.
  • A reserved scope on the shared registry, or a private registry on a verified custom domain. The registry setup guide gives the steps.
  • A publisher key (prt_…) to publish and a download key (pdk_…) to install.

The examples use the organization Acme, the scope @acme, the package @acme/sdk and the shared registry at registry.privatenpm.com.

How a private npm registry works

npm uses a small set of HTTP requests. When you know these requests, you can find the cause of most install and publish failures. This section describes each request and the checks that PrivateNPM.com does.

The metadata request

For each dependency, npm first requests the package document, also called the packument. The packument lists the versions of the package, the dist-tags such as latest, and the tarball address of each version. For a scoped name, npm encodes the slash in the path as %2f.

GET /@acme%2fsdk HTTP/1.1
Host: registry.privatenpm.com
Accept: application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8
Authorization: Bearer pdk_...

The application/vnd.npm.install-v1+json media type asks for the abbreviated packument. The abbreviated document holds only the fields that an install needs, such as dependencies, engines and the dist object. The registry answers with a JSON document like this example:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-cache
ETag: W/"3-abbr-..."

{
  "name": "@acme/sdk",
  "dist-tags": { "latest": "1.2.0" },
  "versions": {
    "1.2.0": {
      "name": "@acme/sdk",
      "version": "1.2.0",
      "dist": {
        "tarball": "https://registry.privatenpm.com/@acme/sdk/-/sdk-1.2.0.tgz",
        "integrity": "sha512-...",
        "shasum": "..."
      }
    }
  }
}

The Cache-Control: private, no-cache header tells npm and proxies to revalidate the document before each use. npm sends the ETag back in an If-None-Match header. If the package did not change, the registry answers 304 Not Modified with no body.

The tarball request

npm selects a version that agrees with the semver range in package.json or the version in the lockfile. Then npm downloads the tarball from the address in dist.tarball. A tarball is a gzip-compressed tar archive of the package files.

GET /@acme/sdk/-/sdk-1.2.0.tgz HTTP/1.1
Host: registry.privatenpm.com
Authorization: Bearer pdk_...

After the download, npm calculates the SHA-512 digest of the bytes and compares it with dist.integrity. If the digests are different, npm stops the install. npm writes the tarball address and the integrity value into package-lock.json. Later installs from the lockfile request the same address and expect the same digest.

PrivateNPM.com writes each tarball address with the hostname that the metadata request used. Thus the tarball request goes to the same host as the metadata request, and npm attaches the same credential. The registry also accepts HTTP range requests, so an interrupted download can continue.

How npm selects a registry for each package

npm sends every package name to the default registry, which is https://registry.npmjs.org/, unless a scope line changes the destination. A scope is the name prefix before the slash, such as @acme. This line in .npmrc sends every @acme package to the private host:

@acme:registry=https://registry.privatenpm.com/

Packages without that scope, such as react, continue to resolve from npmjs.org. The scope line is strict. If the private host answers 404 for @acme/sdk, npm stops with an error. npm does not try the default registry for that name.

An unscoped private package has no prefix for npm to route. To install an unscoped package from a private registry, change the default registry with registry=https://npm.acme.com/. That line also sends all public dependencies to the private host. Add the line only to the projects that need it.

How npm attaches credentials to a host

npm attaches a credential only to requests for the host that the credential line names. The credential line starts with two slashes, has no protocol, and ends with :_authToken:

//registry.privatenpm.com/:_authToken=${ACME_NPM_TOKEN}

npm reads the ACME_NPM_TOKEN environment variable when it reads the file. npm then sends the value as Authorization: Bearer … to that host only. If the two hosts are different, npm sends the request with no credential. The registry then answers 401. Current npm versions do not accept a credential line that does not name a host.

The .npmrc guide explains the file locations, environment variables and the settings for pnpm, Yarn and CI.

What the registry checks

PrivateNPM.com accepts only bearer credentials. For each request, the registry does these checks in this sequence:

  1. PrivateNPM.com finds the registry that the hostname serves.
  2. The registry resolves the credential. An absent, unknown, expired or revoked key gets 401 unauthorized.
  3. The registry compares the registry of the key with the registry of the host. Each key works with one registry only.
  4. The registry finds the package. A package of a different organization gets 404, as if the name does not exist.
  5. The registry compares the operation with the actions of the key and the package access of the key.
  6. For a publisher key, the registry also examines the current permissions of the member who owns the key.

A refused request gets 403 with a stable reason, for example {"error":"forbidden","reason":"not_entitled"}. The 404 answer for the packages of other organizations is intentional. With a 403 answer, an attacker can learn that a private name exists.

The registry fails closed. If an internal check cannot complete, the registry answers with a 5xx status. The registry never sends a private name to npmjs.org as a fallback. If you enable public npm caching, the registry sends only external names upstream. Names that your registry hosts and scopes that an organization reserved never go upstream.

Publish, immutability and tombstones

npm publish sends one PUT request to the package path. The JSON body holds the manifest of the new version, the dist-tag to set, and the tarball as a base64 attachment.

PUT /@acme%2fsdk HTTP/1.1
Host: registry.privatenpm.com
Authorization: Bearer prt_...
Content-Type: application/json

{"name":"@acme/sdk","versions":{"1.2.0":{...}},"dist-tags":{"latest":"1.2.0"},"_attachments":{"sdk-1.2.0.tgz":{"data":"H4sI..."}}}

The registry accepts exactly one version and one tarball in each publish. The registry verifies that the tarball agrees with the integrity value in the manifest. Then the registry stores the version, its storage reference, its dist-tags and an audit entry in one transaction.

The content of a published version is immutable. If you publish the same version again, the registry refuses the request. To change a release, publish a new version. When you unpublish a version, the registry removes the version from the metadata and keeps a tombstone. The tombstone prevents a new publish of that version number. When you unpublish a whole package, the registry keeps a tombstone for the name.

NOTE: Immutability protects the lockfiles of your recipients. A version that a lockfile records always has the same bytes and the same integrity value.

Separate the publish and install credentials

Publishers need permission to create versions. Recipients need permission to read metadata and download tarballs only. Use a different credential for each job. PrivateNPM.com has three kinds of credential:

CredentialPrefixWhat it can doTypical holder
Publisher keyprt_…Read, publish, write dist-tags and unpublish, as its actions allow, in one registryA developer machine or a CI release job
Download keypdk_…Read metadata and download tarballs of the packages that its access rule grantsA recipient, a team or a CI build
API keygsk_…Call the management API, for example to create download keysYour own server or automation

A publisher key acts for the member who created it. The key never gets more permission than that member has now. If the member leaves the organization, the registry refuses the key. A download key belongs to no member. The access rule of a download key grants all packages in one registry, one npm scope, or selected packages.

An API key cannot install or publish packages, and a registry key cannot call the management API. This separation limits the damage if one secret leaks. Trusted publishing with OIDC removes the stored publisher key from a supported CI release job.

A basic rollout

Before you start this procedure, make sure that you have the admin role in your organization. The admin role can create registries, scopes and keys.

  1. Select a scope and a registry address. For example, reserve @acme on registry.privatenpm.com.
  2. Create a publisher key for a person or a CI release job.
  3. Set the package name to @acme/sdk.
  4. Run npm publish. npm prints + @acme/[email protected].
  5. Create a download key for each team, integration or recipient that installs the package.
  6. Add the scope line and the credential line to the .npmrc file of each project that installs the package.
  7. Install the package in a clean environment. The procedure in Verify the result gives the steps.
  8. Record who owns each key, which packages it grants, and how to rotate it.

The registry setup guide and the publish guide give each step with the exact UI path.

Use private packages across a team

Start with one shared library and one application that depends on it. Publish a version of the library. Then install the library from a clean checkout of the application. Expand to more packages only after this test is successful.

Commit the scope line and the credential line with a variable reference in the project .npmrc. Every developer and CI runner then resolves packages from the same host. Each person supplies the secret through the ACME_NPM_TOKEN environment variable or the user file ~/.npmrc.

# .npmrc (committed)
@acme:registry=https://registry.privatenpm.com/
//registry.privatenpm.com/:_authToken=${ACME_NPM_TOKEN}

Keep the release job separate from the application build. The release job needs a publisher key. The build needs only a download key. If one CI job does both, give each step its own variable, so the install step never holds publish permission.

Give each recipient or integration its own download key. Do not share one key across the team. With one key for each holder, you can revoke one holder and keep the others. The download totals for each key also stay accurate.

Verify the result

A test on your own machine can succeed because of a cached package or an old credential in ~/.npmrc. Use a clean environment to prove that the configuration works.

  1. Start a new container: docker run --rm -it node:24 bash.
  2. Create an empty directory.
  3. In that directory, run npm init -y.
  4. Write the scope line and the credential line into .npmrc in that directory.
  5. Set the download key: export ACME_NPM_TOKEN=pdk_....
  6. Run npm whoami --registry=https://registry.privatenpm.com/. npm prints download:rdk_…, which identifies the download key.
  7. Run npm view @acme/sdk version. npm prints the version that the latest dist-tag points to.
  8. Run npm install @acme/sdk. npm adds the package and prints no error.

For a publisher key, npm whoami prints the email address of the member who owns the key. The download also appears under Registry → Activity after a short delay.

Troubleshoot

SymptomCauseCorrection
E401 or 401 Unauthorizednpm sent no credential, or the key is unknown, expired or revoked.Make the credential host the same as the scope host. Make sure that the variable is set in the environment of npm.
E403 with registry_not_in_token_audienceThe key belongs to a different registry.Use a key from the registry that serves this host.
E403 with not_entitledThe access rule of the download key does not include the package.Edit the package access of the key, or issue a key with the correct access rule.
E404 on a private packageThe scope line is absent, the name is wrong, or the package belongs to a different organization.Run npm config get @acme:registry and compare the name with the package list.
EPUBLISHCONFLICT or 403 on publishThe version exists or was unpublished before.Increase the version in package.json and publish again.
EINTEGRITYThe lockfile holds a digest that is different from the downloaded bytes.Examine the resolved address in the lockfile. Make sure that it points to the correct registry host.

For a full diagnosis of authentication failures, use the npm E401 guide.

Security and maintenance

WARNING: Do not commit a key to source control. Anyone who can read the project can then install your packages or publish in your name.

  • Give each key the smallest access that its job needs. Limit a publisher key to one scope or one package when possible.
  • Do not select the Unpublish action for CI publisher keys. Unpublish is a separate choice on the key.
  • Set an expiry date on keys for contractors and short projects.
  • Rotate a key when a person who knew the secret leaves, or when a log shows the secret. Rotation issues a new secret.
  • Revoke a key when the holder must lose access. The registry refuses the key after a short authorization lease.
  • Examine the organization audit log and Registry → Activity to see who published and who downloaded.

NOTE: Revocation stops new downloads. Revocation cannot remove package copies that a recipient installed before or that a build cache holds.

Choose a registry for the way you ship

  • Internal libraries: examine package manager support, team access, CI authentication, and what happens to publish rights when a person leaves.
  • Commercial SDKs: make sure that you can grant one recipient selected packages without organization membership. PrivateNPM.com gives read-only download keys and a recipient download portal.
  • Operations: decide if your team must operate the service. A custom domain changes the address only. A self-hosted registry is a different requirement.
  • Budget: compare your number of packages, private registries, download keys and bandwidth with the plan limits.

The feature list shows all registry functions. It also describes the Python and file registries.

READY WHEN YOU ARE

Put private packages to work.