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

Publishing Scoped Packages to npm

Scoped packages — names of the form @scope/name — give you a private namespace under a user or organization, eliminating the global name collisions that plague unscoped names. The trade-off is one extra rule: scoped packages publish privately by default, so a public first publish needs an explicit access flag. This page walks through the symptoms of getting that wrong, the exact steps to publish correctly, and the guardrails that keep it reliable in CI.

Symptoms

A scoped first publish that omits the public access opt-in fails immediately, before any tarball is uploaded:

Symptoms A scoped first publish that omits the public access opt-in fails immediately, before any tarball is uploaded: Symptoms A scoped first publish that omits the public access opt-in fails immediately, before any tarball is uploaded:
Symptoms — the core idea of this section at a glance.
npm error code E402
npm error 402 Payment Required - PUT https://registry.npmjs.org/@acme%2fwidget
npm error You must sign up for private packages

Or, when the scope exists but the access level is wrong for your plan:

npm error code E403
npm error 403 Forbidden - PUT https://registry.npmjs.org/@acme%2fwidget
npm error You do not have permission to publish "@acme/widget".

The 402 Payment Required message is the tell: npm is trying to publish a private scoped package and is asking you to pay for private hosting, when what you actually want is a public scoped package, which is free.

Root Cause

Scoped packages default to restricted (private) access. Unscoped packages default to public. This single difference, rooted in how npm models the @scope namespace, is the source of nearly every scoped first-publish failure. The registry will not infer that you meant "public" — you must declare it. Understanding default access alongside the rest of the publish lifecycle is covered in npm Registry Publishing Workflows.

Scoped publish decision A scoped package name routes to a default restricted access check; declaring public access yields a successful public publish, otherwise it fails. @scope/name defaults restricted access public? flag or config public publish free, visible 402 / 403 publish rejected yes no
A scoped name is restricted until you declare public access; omitting that declaration is what triggers the 402/403 failure.

Scoped packages behave differently from unscoped ones in one way that trips up a first publish: they default to restricted (private) access, so publishing a scoped package intended to be public without specifying access fails or publishes privately. A scope like @acme namespaces the package to your user or organization, which is what enables private packages and per-scope registry routing, but it also means the registry needs to be told explicitly when a scoped package should be public.

The second common cause of a failed scoped publish is authentication scoped to the wrong identity. A scoped package under an organization requires the publishing token to have publish rights for that organization, and a personal token without organization access is rejected. Because the scope determines both the namespace and the access-control boundary, a scoped publish that works for a personal scope can fail for an organization scope purely because the token's permissions do not extend to the org.

Resolution Steps

  1. Confirm the name is correctly scoped. The name field must be @scope/name, all lowercase, with the scope matching a user or organization you belong to:
    {
      "name": "@acme/widget",
      "version": "1.0.0"
    }
  2. Declare public access in the manifest. Pinning it in publishConfig means you never have to remember the flag again:
    {
      "publishConfig": {
        "access": "public"
      }
    }
  3. Or pass the flag explicitly on the publish command:
    npm publish --access public
  4. Verify the tarball before uploading so a scoped fix does not coincide with a contents bug:
    npm pack --dry-run
  5. Publish. With either the config or the flag in place, the public publish succeeds:
    npm publish
Resolution Steps Resolution Steps in production JavaScript package workflows. Resolution Steps Resolution Steps in production JavaScript package workflows.
Resolution Steps — the core idea of this section at a glance.

Declare the access level explicitly, either on the command line or in the manifest:

# Publish a scoped package as public
npm publish --access public
// package.json — make the intent durable
{
  "name": "@acme/ui",
  "publishConfig": { "access": "public" }
}

Setting publishConfig.access to public in the manifest means every publish uses the right access level regardless of the command, and for a genuinely private package, restricted keeps it off the public index. Use a token with publish rights for the scope's organization.

Validation

Confirm the package is public and resolvable:

Validation Confirm the package is public and resolvable: Validation Confirm the package is public and resolvable:
Validation — the core idea of this section at a glance.
# Inspect the published access level and dist-tags
npm view @acme/widget

# Confirm it installs in a clean directory without auth
cd "$(mktemp -d)" && npm install @acme/widget

If npm view returns metadata anonymously, the package is public. If it 404s for logged-out users but resolves while authenticated, it published as restricted — set access to public and republish a new version (you cannot change access by re-pushing the same version; bump it).

Organization Scopes

When the scope is an npm organization (@acme) rather than a personal scope, two extra factors apply. First, your account must be a member of the org with publish rights to that package or team. Second, the org's default package visibility setting can be configured to public, which removes the per-publish access requirement for everyone on the team — useful for open-source orgs that never publish private packages. A granular access token scoped to the organization is the right credential for CI; broader token scope and 2FA mechanics are detailed in npm Registry Publishing Workflows.

Organization Scopes When the scope is an npm organization (@acme) rather than a personal scope, two extra factors apply. Organization Scopes When the scope is an npm organization (@acme) rather than a personal scope, two extra factors apply.
Organization Scopes — the core idea of this section at a glance.

Guardrails

  • Set publishConfig.access: "public" in every public scoped package's manifest so the access level is version-controlled, not flag-dependent.
  • Run npm publish --dry-run in CI on every pull request to catch access and contents problems before a tag triggers a real publish.
  • For organizations, set the org-wide default package access to public if you never publish private packages, removing a recurring footgun.
  • Keep the name scope consistent across a monorepo so every package inherits the same publish policy; the wider release setup lives under Package Publishing & Release Engineering.
Guardrails Guardrails in production JavaScript package workflows. Guardrails Guardrails in production JavaScript package workflows.
Guardrails — the core idea of this section at a glance.

Scoped packages, private registries, and access control

A scope is the unit that private-registry routing and access control both key on, which is why scoping is a foundational decision rather than a naming preference. Because an entire scope can be mapped to a registry, @acme/* can be routed to a private registry while everything else resolves publicly, and the scope's access level — public or restricted — governs visibility on whichever registry hosts it. For anything internal, scoping is what makes an unambiguous private mapping possible and what closes the dependency-confusion vector where an internal name could be resolved from a colliding public package.

Scope routing How scope and access decide where a package goes. What is the scoped package? internal private registry + restricted open-source public registry + public unscoped global namespace
Scope drives both private routing and access control, so decide it at creation.

The practical consequence is that deciding a package's scope and access at creation time, and encoding it in the manifest, prevents a class of irreversible mistakes. A scoped package with publishConfig.access set to restricted and private: true during development cannot be accidentally published to the public index, and pinning publishConfig.registry fixes where it goes regardless of a developer's default. Treating the scope, the access level, and the registry as one coherent decision — encoded in the manifest rather than left to a publish command — is what keeps proprietary packages private and public ones deliberately open.

Publishing scoped packages from CI

Publishing a scoped package from CI rather than a laptop keeps the credential out of personal environments and makes the release reproducible. The job authenticates to the registry with a token scoped to the organization — or, better, an OIDC identity that grants short-lived publish rights — installs with a frozen lockfile and ignored scripts, and publishes with the access level fixed by publishConfig. Setting the registry on the Node setup step ensures both installs and the publish target the right host, which matters for a scope routed to an organization or private registry.

CI scoped publish Org-scoped credential, fixed access, frozen install. org token / OIDC scope rights publishConfig.access fixed level publish reproducible
Automating the scoped publish removes the wrong-access and wrong-credential mistakes.

The safety of the CI publish comes from every input being derived or verified: the version computed from the changes so it cannot collide, the access level fixed in the manifest so a scoped package cannot leak public, the tarball contents from the files allowlist, and — with provenance — a signed link back to the source. For a scoped package specifically, this removes the two most common manual mistakes: publishing with the wrong access level and publishing with a credential that lacks organization rights. Automating the scoped publish this way turns it into a repeatable, auditable operation rather than a hand-typed command that can go wrong in ways that are hard to reverse.

Free public scopes versus paid private packages

npm's pricing shapes how scoped packages are used, and knowing the model avoids a surprise at publish time. Publishing a scoped package as public is free — an organization or user scope can host any number of public packages at no cost, which is how most open-source scoped packages are published. Publishing a scoped package as private (restricted) requires a paid plan, because private packages are a billed feature. This is why a scoped publish that omits --access public can fail with a payment-related error: the registry defaults the scoped package to private, which needs a paid account.

Public vs private cost How access level relates to npm pricing. Access Cost Use public free open source private (npm) paid plan closed source private (self-host) your infra full control
Public scoped packages are free; private ones need a paid plan or a private registry.

The practical takeaway is to be explicit about access to match your intent and your plan. An open-source scoped package should declare publishConfig.access as public so it publishes freely; a genuinely private package needs a paid npm plan or a private registry that hosts it. For teams that want private packages without npm's paid plan, routing a scope to a self-hosted registry or GitHub Packages is the common alternative. Deciding public-versus-private at creation, and declaring it in the manifest, means the publish matches both what you intend and what your account supports.

Frequently Asked Questions

Why does npm ask for payment (402) when I publish a scoped package? Because scoped packages default to restricted (private) access, and private packages require a paid plan. You almost certainly want a public scoped package, which is free — declare it with npm publish --access public or publishConfig.access: "public" and the 402 disappears.

Do I need a paid npm account to publish a public scoped package? No. Public scoped packages are free. Payment is only required for private packages. The 402 error is npm interpreting the default restricted access as a private-package request.

Can I change a scoped package from restricted to public after publishing? You change the access level with npm access public @scope/name, but the cleanest path is to set publishConfig.access: "public" and publish a new version. Access changes apply going forward; you cannot rewrite an already-published version's tarball.

Should I use my username or an organization as the scope? For solo projects a personal scope (@yourname) is fine. For team-owned packages use an organization scope so membership, teams, and publish rights are managed centrally and a single org-wide default access setting governs every package.

Why does my scoped package publish privately or fail?

Scoped packages default to restricted access. Publish a public one with npm publish --access public, or set publishConfig.access to public in the manifest so every publish uses the right level. For an org scope, ensure your token has publish rights for that organization.

Should internal packages be scoped?

Almost always. A scope namespaces the package to your organization, enables per-scope private-registry routing, and closes dependency confusion by making resolution unambiguous. Set its access to restricted and pin publishConfig.registry so it cannot leak to the public index.

How do I stop a scoped package from being published publicly by accident?

Set publishConfig.access to restricted and private: true during development, and pin publishConfig.registry to the intended host. Publishing from CI with a scoped credential rather than a laptop removes the last manual chance to get the access level wrong.

Why does publishing my scoped package ask for payment?

Because it defaulted to private (restricted), and private packages on npm require a paid plan. Publish it as public with --access public or publishConfig.access: public if it is open-source; for a genuinely private package, use a paid plan or route the scope to a private registry.

How do consumers install my scoped package?

Exactly like any package: npm install @acme/ui. If it is public, no configuration is needed; if it is private or on a private registry, consumers need the scope mapped to that registry and a token with read access, which is the per-scope configuration a private registry uses.

Can I change a scoped package from private to public later?

Yes — npm access public @acme/pkg changes an existing package's access, and updating publishConfig.access keeps future publishes public. Going the other way, from public to private, is also possible but requires a paid plan since private packages are billed.

Related

npm Registry Publishing Workflows