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:
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:
-
Open
authUrl, check the change, and select Approve with passkey. -
Get the one-time code:
curl "$DONE_URL"The answer is
{ "token": "…" }. -
Send the same request again, with the same body, and add the code:
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:
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:
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:
curl https://registry.example.com/-/registry/acme \
-H "authorization: Bearer $NPM_TOKEN"{
"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
alicefor@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:
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:
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:
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, andnpm auditshows an error.npm installstill 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.comgets404after the move. Add the@acme:registryline to each.npmrcand 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.