Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

Private Registries and Access Control

Publishing internal packages to the public npm registry leaks your code and namespace; publishing them with no access controls invites supply-chain compromise. This guide covers running and consuming a private registry — GitHub Packages, a self-hosted Verdaccio, or scoped npm org packages — with correct authentication, and it extends the Package Publishing & Release Engineering section toward internal distribution.

Concept overview and where it fits

A private registry solves two problems at once: it keeps proprietary packages off the public index, and it gives you a controlled resolution path so installs cannot silently pull a same-named package from elsewhere. Most teams route scoped packages (@acme/*) to a private registry while everything else resolves from the public npm registry, using per-scope registry configuration. The scoping mechanics build directly on the npm registry publishing workflow.

Per-scope routing Which registry a request resolves to by scope. What scope is the package? @acme/* private registry public dep registry.npmjs.org unscoped default registry
Only your scope hits the private host; everything else stays public.

Initialization and per-scope configuration

Configure resolution per scope in .npmrc so only your namespace hits the private host. Keep the auth token in an environment variable, never committed.

Initialization and per-scope configuration Configure resolution per scope in .npmrc so only your namespace hits the private host. Initialization and per-scope configuration Configure resolution per scope in .npmrc so only your namespace hits the private host.
Initialization and per-scope configuration — the core idea of this section at a glance.
# .npmrc
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
# everything else still resolves from the public registry
registry=https://registry.npmjs.org/

The ${NODE_AUTH_TOKEN} reference is expanded from the environment at install time, so the token lives in your secret store, not the repo.

Architecture: how a registry resolves a package

A registry is content-addressed metadata plus tarballs: the client asks for a package version, the registry returns the manifest and a tarball URL, and the client verifies the integrity hash against the lockfile. A private registry either stores tarballs itself (Verdaccio, GitHub Packages) or proxies the public registry for anything it does not host, so a single configured host can serve both your packages and their public dependencies.

Registry resolution Request, manifest, tarball, integrity verify. request version client asks manifest + tarball URL registry responds download + verify integrity hash
A registry returns metadata and a tarball the client verifies against the lockfile.

Execution strategy: publish access levels

Set publish-time access explicitly so a scoped package does not accidentally go public. For npm orgs, --access restricted keeps it private; for GitHub Packages, repository visibility governs it.

Execution strategy: publish access levels Set publish-time access explicitly so a scoped package does not accidentally go public. Execution strategy: publish access levels Set publish-time access explicitly so a scoped package does not accidentally go public.
Execution strategy: publish access levels — the core idea of this section at a glance.
// package.json
{
  "name": "@acme/ui",
  "version": "2.3.0",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com",
    "access": "restricted"
  }
}

Security and isolation

Access control is the whole point, so treat tokens as the crown jewels. Use short-lived, scoped tokens (GitHub's GITHUB_TOKEN, npm granular access tokens) rather than a personal token with publish rights across everything, require 2FA for human publishes, and prefer OIDC-based provenance so a published tarball is cryptographically tied to the CI build that produced it. Lock the resolution path with a per-scope registry so a typosquat cannot be resolved from an unexpected host.

Access-control layers Scoped token, 2FA/OIDC, access level, pinned host. scoped short-lived token CI mints per job 2FA / OIDC identity proof access: restricted private by default pinned per-scope host no rogue resolution
Stack short-lived tokens, 2FA, restricted access and a pinned host.

CI/CD integration

Publish from CI with a token minted for the job, never a developer's laptop credential:

CI/CD integration Publish from CI with a token minted for the job, never a developer's laptop credential: CI/CD integration Publish from CI with a token minted for the job, never a developer's laptop credential:
CI/CD integration — the core idea of this section at a glance.
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          registry-url: https://npm.pkg.github.com
      - run: pnpm install --frozen-lockfile --ignore-scripts
      - run: pnpm publish --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Common Pitfalls and Remediation

Mistake Impact Remediation
Token committed in .npmrc Credential leak, unauthorized publish Reference ${NODE_AUTH_TOKEN}; store the token in secrets.
No per-scope registry Typosquat resolves from a wrong host Pin @scope:registry so only your namespace hits the private host.
Publishing scoped packages as public by default Proprietary code leaks Set publishConfig.access to restricted.
Long-lived personal publish token Broad blast radius if leaked Use short-lived, scoped CI tokens with 2FA.
Common Pitfalls and Remediation Common Pitfalls and Remediation in production JavaScript package workflows. Common Pitfalls and Remediation Common Pitfalls and Remediation in production JavaScript package workflows.
Common Pitfalls and Remediation — the core idea of this section at a glance.

Frequently Asked Questions

GitHub Packages, Verdaccio, or an npm org — which should I choose?

Choose GitHub Packages if your code already lives on GitHub and you want zero infrastructure; Verdaccio if you want a self-hosted proxy with full control; an npm org if you want scoped private packages on the canonical registry with granular tokens.

Can I mix private and public dependencies?

Yes. Route only your scope (@acme:registry=...) to the private host and leave the default registry public, so private packages and their public dependencies both resolve from one install.

Why use per-scope registry config instead of a global one?

A global registry override sends every request — including public dependencies — to one host, which is slower and a larger trust surface. Per-scope config keeps the blast radius to your namespace.

How do I stop an internal package from being published publicly by accident?

Set publishConfig.access to restricted and private: true during development, and gate publishing behind a CI job that uses a scoped token rather than a developer's global credential.

Related

Package Publishing & Release Engineering