| |

npm 23 ๐ŸŒ Scoped Packages (@org/package)

โฑ๏ธ Estimated read: 10 min | |

A scoped package is a package whose name is prefixed with a namespace, written as @scope/package-name. The scope is the part between the @ and the /, and it groups related packages under a single namespace. Scopes exist to prevent name collisions: a package named utils is unique, but @myorg/utils and @otherorg/utils can coexist because they live in different scopes . Every npm user and every npm organization is granted a scope matching its name, and only the owner of that scope can publish packages into it .

This chapter covers the name format, the installation behavior, the visibility defaults, the access flags required for publishing, and the registry association that makes multi-registry workflows possible. It examines why the scope is a naming mechanism rather than a visibility mechanism, and how the two interact at publish time.

Key point: Scoped packages are private by default. Publishing a scoped package to the public registry requires --access public on the first publish. Without the flag, the registry attempts to create a private package, which fails on a free account and succeeds as private on a paid account.


Why scoped packages exist

The name collision problem. Package names in the public registry are first-come, first-served. A name like utils is already taken, and a new package with that name cannot be published. Scopes solve this by adding a namespace in front of the name. @myorg/utils and @otherorg/utils are distinct packages, even though both end in utils . The scope is granted to a user or organization, so only that entity can publish packages into it.

The grouping problem. An organization that publishes multiple related packagesโ€”a CLI, a shared library, a config packageโ€”can group them under a single scope. @myorg/cli, @myorg/core, and @myorg/config are visibly related, and a consumer can install all three with npm install @myorg/cli @myorg/core @myorg/config. The scope communicates ownership and relationship without requiring a naming convention inside the package names themselves.

The multi-registry problem. A project that depends on both public packages and private packages needs to resolve each from the correct registry. Scopes provide the association mechanism: @myorg:registry=https://npm.company.com/ directs all @myorg packages to the company registry, while unscoped packages continue to resolve from the public registry . Without scopes, a project would need to switch the default registry before each install, which is impractical for a mixed dependency tree.


a. The Name Format and Installation

The scope follows the same naming rules as package names: URL-safe characters, no leading dots or underscores. The full name is written as @scope/package-name. When installed, the package is placed in a subdirectory of node_modules named after the scope, so @myorg/mypackage is installed to node_modules/@myorg/mypackage .

# Install a scoped package
npm install @myorg/mypackage

# In package.json
# "dependencies": { "@myorg/mypackage": "^1.3.0" }

The @ symbol is mandatory in both the install command and the package.json dependency. Omitting it causes npm to interpret the name as a GitHub repository reference rather than a registry package . In application code, the scope must also be included in the require or import statement, because Node treats the scope folder as an ordinary directory.

// Requiring a scoped package
const mypackage = require('@myorg/mypackage');

b. Visibility and Access Levels

The visibility of a scoped package is determined by its access level, which is set at publish time. Scoped packages default to restricted (private). Unscoped packages are always public and cannot be made private.

Scope TypeAccess LevelWho Can ViewWho Can Publish
OrganizationPrivateTeam members with read accessTeam members with write access
OrganizationPublicEveryoneTeam members with write access
UserPrivateOwner and granted usersOwner and granted users
UserPublicEveryoneOwner and granted users

To publish a scoped package as public on the primary registry, the first publish must include the --access public flag . Subsequent version publishes do not require the flag because the access level is already set.

# First publish: make the scoped package public
npm publish --access public

# Private publish (requires paid account for user-scoped, or paid org)
npm publish

If the account is free and the package is scoped, npm publish without --access public fails with an HTTP 402 status code. The registry interprets the default restricted access as a request for a private package, which the free account cannot create . The access level can be changed after publishing with npm access set status=public or status=private .


c. Associating a Scope with a Registry

A scope can be associated with a specific registry, which allows a project to install public packages from the default registry and private packages from a company registry without switching configuration between commands. The association is set in .npmrc with the @scope:registry key .

# .npmrc
@mycompany:registry=https://npm.company.com/

Once the association is set, any install or publish for a package whose name begins with @mycompany goes to npm.company.com instead of the default registry. The relationship is many-to-one: one registry can host multiple scopes, but a scope points to exactly one registry.

For private registries that require authentication, the token must be scoped to the registry host using the //host/:_authToken syntax. The scoped token is sent only to the registry that the scope is associated with, which prevents credentials from leaking to the public registry .


Complete Example Session

# ============================================
# PART 1: CREATE A SCOPED PACKAGE
# ============================================
mkdir my-package && cd my-package
npm init --scope=@myorg
# package.json: { "name": "@myorg/my-package", ... }


# ============================================
# PART 2: PUBLISH AS PUBLIC (FREE ACCOUNT)
# ============================================
npm publish --access public
# Published to registry.npmjs.org/@myorg/my-package


# ============================================
# PART 3: VERIFY THE ACCESS LEVEL
# ============================================
npm access get status @myorg/my-package
# public


# ============================================
# PART 4: ASSOCIATE THE SCOPE WITH A PRIVATE REGISTRY
# ============================================
npm config set @myorg:registry https://npm.company.com/


# ============================================
# PART 5: PUBLISH TO THE PRIVATE REGISTRY
# ============================================
npm publish
# Published to npm.company.com/@myorg/my-package


# ============================================
# PART 6: INSTALL THE SCOPED PACKAGE
# ============================================
npm install @myorg/my-package
# Installed to node_modules/@myorg/my-package


# ============================================
# PART 7: GRANT TEAM ACCESS (ORG)
# ============================================
npm access grant read-write myorg:developers @myorg/my-package


# ============================================
# PART 8: REVOKE ACCESS
# ============================================
npm access revoke myorg:developers @myorg/my-package

The eight parts trace the scoped package from creation through public publish, private registry association, and team access management. The critical steps are Part 2 (--access public) and Part 4 (registry association).


Quick Reference

Scoped Package Commands

CommandPurpose
npm init --scope=@orgInitialize with scope
npm publish --access publicPublish scoped package as public
npm publishPublish as private (paid account)
npm access get status @org/pkgCheck access level
npm access set status=public @org/pkgChange to public
npm install @org/pkgInstall scoped package

Scope Registry Configuration

KeyPurpose
@org:registry=https://...Associate scope with registry
npm login --scope=@orgAssociate at login
//host/:_authToken=...Scoped authentication token

Access Levels

LevelWho Can ViewWho Can Publish
PublicEveryoneOwner/team with write
PrivateOwner/team with readOwner/team with write

Best Practices

โœ… Do This:

# Use --access public on first scoped publish
npm publish --access public                                            # โœ…
# Associate the scope with the registry in .npmrc
@myorg:registry=https://npm.company.com/                               # โœ…
// Use the full scoped name in require/import
require('@myorg/my-package')                                           // โœ…

โŒ Don’t Do This:

# Publish scoped package without --access public on free account
npm publish  # HTTP 402                                                # โŒ
# Omit the @ in install
npm install myorg/my-package  # installs from GitHub, not registry      # โŒ
# Assume scoped packages are public by default
# They are restricted by default                                        # โŒ

Common Pitfalls

PitfallWhy It HappensFix
402 error on publishFree account, no --access publicAdd the flag
Installed from GitHub instead of registry@ omitted from nameUse @scope/name
Cannot publish to org scopeNot a member or no write accessJoin org, request team access
Scope associated with wrong registry.npmrc precedenceCheck npm config get @scope:registry
Private package viewable by othersAccess level not set correctlynpm access set status=private

Real-World Examples

1. Initialize Scoped Package

npm init --scope=@myorg

2. Publish Public

npm publish --access public

3. Publish Private

npm publish

4. Install Scoped

npm install @myorg/mypackage

5. Associate Registry

@myorg:registry=https://npm.company.com/

6. Check Access

npm access get status @myorg/mypackage

7. Change to Public

npm access set status=public @myorg/mypackage

8. Grant Team Access

npm access grant read-write myorg:developers @myorg/mypackage

9. Scoped Token

//npm.company.com/:_authToken=${NPM_TOKEN}

10. Require Scoped Package

require('@myorg/mypackage')

Visual

Scope Namespace

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  @myorg/                    @otherorg/                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  โ”‚ utils           โ”‚        โ”‚ utils           โ”‚             โ”‚
โ”‚  โ”‚ logger          โ”‚        โ”‚ logger          โ”‚             โ”‚
โ”‚  โ”‚ config          โ”‚        โ”‚ config          โ”‚             โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚                                                              โ”‚
โ”‚  Same package names, different scopes.                       โ”‚
โ”‚  No collision because the scope is part of the name.         โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Scope to Registry Association

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  .npmrc                                                      โ”‚
โ”‚  @myorg:registry=https://npm.company.com/                    โ”‚
โ”‚                                                              โ”‚
โ”‚  npm install @myorg/utils                                    โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Scope matches?  โ”‚โ”€โ”€โ”€โ”€โ–บ YES: use company registry         โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚ NO                                               โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Use default     โ”‚                                        โ”‚
โ”‚  โ”‚ registry        โ”‚                                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  One scope, one registry. Multiple scopes can share one.     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Publish Access Decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SCOPED PACKAGE                                              โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ --access public โ”‚โ”€โ”€โ”€โ”€โ–บ YES: public package               โ”‚
โ”‚  โ”‚ on first publishโ”‚      (free account works)              โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚ NO                                               โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Paid account?   โ”‚โ”€โ”€โ”€โ”€โ–บ YES: private package              โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚ NO                                               โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ HTTP 402        โ”‚                                        โ”‚
โ”‚  โ”‚ Payment Requiredโ”‚                                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  The flag is mandatory for public scoped packages on free.   โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Installation Path

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  node_modules/                                               โ”‚
โ”‚  โ”œโ”€โ”€ lodash/                    โ† unscoped                  โ”‚
โ”‚  โ””โ”€โ”€ @myorg/                    โ† scope folder              โ”‚
โ”‚      โ”œโ”€โ”€ utils/                                             โ”‚
โ”‚      โ”œโ”€โ”€ logger/                                            โ”‚
โ”‚      โ””โ”€โ”€ config/                                            โ”‚
โ”‚                                                              โ”‚
โ”‚  The scope is a directory inside node_modules.              โ”‚
โ”‚  The require path includes it: require('@myorg/utils').     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Scoped name format@scope/package-name
Installation pathnode_modules/@scope/package-name
Default visibilityPrivate (restricted)
Public publish flag--access public (first publish)
Free account + scoped + no flagHTTP 402
Scope to registry@scope:registry in .npmrc
Org scope publishMust be org member with write access
Access change after publishnpm access set status=...

Key takeaways:

  • A scope is a namespace, not a visibility setting. The @org/ prefix prevents name collisions and groups related packages, but it does not determine whether the package is public or private. The access level is a separate decision made at publish time .
  • Scoped packages are private by default. The registry treats a scoped package as restricted unless --access public is passed on the first publish. On a free account, the restricted default causes a 402 error because private packages require a paid plan .
  • The scope can be associated with a registry. The @scope:registry key in .npmrc directs all installs and publishes for that scope to a specific registry. This allows a project to use the public registry for unscoped packages and a private registry for scoped packages without changing the default .
  • Organization scopes require membership. Publishing to @org/package requires being a member of the org organization with write access to the package. The organization must exist on npmjs.com before the package can be published .
  • The @ symbol is not optional. Omitting it from an install command or package.json dependency causes npm to interpret the name as a GitHub reference rather than a registry package .

Remember: A scoped package is a package with a namespace. The namespace is the mechanism that allows multiple organizations and users to publish packages with the same short name without collision. The access level is a separate mechanism that determines who can install the package. The registry association is a third mechanism that determines where the package lives. These three mechanisms are independent: a scoped package can be public or private, and it can live on the public registry or a private one. The --access public flag controls the first. The @scope:registry key controls the second. The scope name itself controls neither; it only provides the namespace.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!