DocsPrivate registries
Docs · Guides

Private registries

Give your scopes their own host, readers, and search index, off the public registry.

Updated Oct 14, 2026docs/public/private-registries.md

A private registry serves one or more scopes that you own, on its own host. Its packages are not on the public registry: the public host answers 404 for them, and public search does not show them. Only the readers of the registry can install, search, or see its packages.

Use a private registry when a scope must not appear on the public registry at all. For a few private packages in a public scope, a restricted package is enough.

Feature Restricted package Private registry
Host registry.example.com acme.registry.example.com
Who can install the scope's user, or the members of its Org the readers of the registry
Search not in public search its own index, for its readers only
Can become public yes no
Copies of public names no yes
Passthrough to public names no when you turn it on

Create a registry#

You need:

  • the token from npm login. A token from Settings, or an Org token, can only read.
  • a passkey to approve the request
  • each scope to be yours: your user name, or an Org where you are an owner
  • each scope to be on the public registry. A scope can belong to one private registry only.

Choose a slug. The slug is the first label of the host, so acme gives acme.registry.example.com. A slug has 1 to 63 lowercase letters, digits, and hyphens. It cannot start or end with a hyphen. These slugs are reserved: public, npm, api, www, and registry.

The website has no page for this yet. Send the request with curl:

Shell
curl -X PUT https://registry.example.com/-/registry/acme \
  -H "authorization: Bearer $NPM_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "scopes": ["acme"] }'

The first answer is 401, with an authUrl and a doneUrl. The request waits for your passkey:

  1. Open authUrl, check the change, and select Approve with passkey.

  2. Get the one-time code:

    Shell
    curl "$DONE_URL"

    The answer is { "token": "…" }.

  3. Send the same request again, with the same body, and add the code:

    Shell
    curl -X PUT https://registry.example.com/-/registry/acme \
      -H "authorization: Bearer $NPM_TOKEN" \
      -H "content-type: application/json" \
      -H "npm-otp: $CODE" \
      -d '{ "scopes": ["acme"] }'

The code works once, for this request only, within 5 minutes.

Status Meaning
201 The registry exists. Its scopes, and their packages, moved to it.
401 The request waits for your passkey, or the code is wrong.
400 The body has no scopes.
403 You do not own each scope.
409 The slug is taken, or a scope already belongs to a private registry.

Caution

The scopes move when you create the registry. The public host stops serving their packages at once, and no route moves a scope back.

Route a scope to the registry#

Add two lines to .npmrc. The first line sends the scope to the private host. The second line gives that host your token:

.npmrc
registry=https://registry.example.com/
//registry.example.com/:_authToken=${NPM_TOKEN}

@acme:registry=https://acme.registry.example.com/
//acme.registry.example.com/:_authToken=${NPM_TOKEN}

The same user token works on each host. npm, pnpm, and Bun read these lines. Yarn reads npmScopes in .yarnrc.yml:

.yarnrc.yml
npmScopes:
  acme:
    npmRegistryServer: "https://acme.registry.example.com"
    npmAuthToken: "${NPM_TOKEN}"

To see the settings of the registry, ask for it with a reader's token:

Shell
curl https://registry.example.com/-/registry/acme \
  -H "authorization: Bearer $NPM_TOKEN"
JSON
{
  "slug": "acme",
  "owner": "alice",
  "scopes": ["acme"],
  "created": "2026-10-12T17:04:11.000Z",
  "passthrough": false,
  "url": "https://acme.registry.example.com"
}

Readers#

A reader can install the packages of the registry, search them, and publish to it. The readers are:

  • the user of each user scope, such as alice for @alice
  • each member of each Org scope, in any role
  • the Org tokens of each Org scope

Each reader uses their own token. A change to the members of an Org applies at the next request.

Reader Install and search Publish
The user of a user scope yes yes, with npm login and a passkey
A member of an Org scope, any role yes yes, with npm login and a passkey
An Org token of an Org scope yes no
A token of a reader, from Settings yes no

To let a machine, such as a build server, install, give it an Org token or a token from Settings. These tokens can only read. To publish from CI, use trusted publishing.

Important

Planned: a reader role that can only install, and customer keys. A customer key is a read-only key for one customer, with a name, an optional expiry, and its own download totals. Today, each member of an Org scope can publish.

Private copies of public names#

A reader can also publish a name that is not in the scopes of the registry, such as a patched copy of lodash. The copy belongs to the registry:

  • Every reader can publish new versions of it.
  • It is always restricted.
  • It never changes the public package of the same name. Other users still get the public lodash.

Publish the copy to the private host:

Shell
npm publish --registry https://acme.registry.example.com/

A name with no scope does not match the @acme:registry line, so a client asks the public host for it. To install the copy, point the whole project at the private host, and turn on passthrough for all other names.

Passthrough#

With passthrough on, the registry answers its readers with the public packages for the names that it does not hold. Then a team can use the private host as its only registry:

.npmrc
registry=https://acme.registry.example.com/
//acme.registry.example.com/:_authToken=${NPM_TOKEN}

Passthrough is off by default. Only the user who made the registry can change it, and the change waits for a passkey, as when you create a registry:

Shell
curl -X PUT https://registry.example.com/-/registry/acme \
  -H "authorization: Bearer $NPM_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "passthrough": true }'

What acme.registry.example.com answers for each kind of name:

Name Passthrough off Passthrough on
@acme/sdk, in a scope of the registry the registry's package, or 404 the registry's package, or 404
lodash, with a copy in the registry the registry's copy the registry's copy
react, with no copy 404 the public package

A name in a scope of the registry never passes through. A name that the registry holds never passes through.

Audits#

npm audit, pnpm audit, and yarn audit work on a private registry with passthrough. The registry sends the audit request on for its readers. Before the request leaves nextpm, the registry removes:

  • each name in a scope that it knows
  • each name that it keeps its own copy of

So the names of your private packages stay inside nextpm.

Note

Security advisories come from the upstream registry. Without an upstream, the audit routes answer 501, and npm audit shows an error. npm install still works.

What moves with a scope#

When a scope moves to a private registry, all of its packages move with it:

Property Before the move After the move
Host registry.example.com acme.registry.example.com
Public host serves the package answers 404
Package id npm:@acme/sdk npm.acme:@acme/sdk
Access public or restricted restricted, and it cannot become public
Search public search, if public the registry's own index
Download counts api.example.com api.example.com, with a reader's token

Each version, tag, and day of download counts moves with its package. New packages in the scope are restricted.

Warning

A client that still sends the scope to registry.example.com gets 404 after the move. Add the @acme:registry line to each .npmrc and each CI config when you create the registry.

Pricing#

A private registry is a paid plan. It costs $49 for each registry each month, plus a $5 Pro seat each month for each member who publishes. It includes its own host and search index, any number of scopes, unlimited readers, and 250 GB of private transfer. See Pricing.

Note

Planned: billing. You cannot buy a plan yet.

API#

Method Path Purpose
PUT /-/registry/:slug Create a registry: { "scopes": ["acme"], "passthrough": false }.
PUT /-/registry/:slug Change a registry: { "passthrough": true }. Only the user who made it.
GET /-/registry/:slug { slug, owner, scopes, created, passthrough, url }. Readers only.

The API reference has the full rules.