npm 23 ๐ Scoped Packages (@org/package)
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 Type | Access Level | Who Can View | Who Can Publish |
|---|---|---|---|
| Organization | Private | Team members with read access | Team members with write access |
| Organization | Public | Everyone | Team members with write access |
| User | Private | Owner and granted users | Owner and granted users |
| User | Public | Everyone | Owner 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
| Command | Purpose |
|---|---|
npm init --scope=@org | Initialize with scope |
npm publish --access public | Publish scoped package as public |
npm publish | Publish as private (paid account) |
npm access get status @org/pkg | Check access level |
npm access set status=public @org/pkg | Change to public |
npm install @org/pkg | Install scoped package |
Scope Registry Configuration
| Key | Purpose |
|---|---|
@org:registry=https://... | Associate scope with registry |
npm login --scope=@org | Associate at login |
//host/:_authToken=... | Scoped authentication token |
Access Levels
| Level | Who Can View | Who Can Publish |
|---|---|---|
| Public | Everyone | Owner/team with write |
| Private | Owner/team with read | Owner/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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| 402 error on publish | Free account, no --access public | Add the flag |
| Installed from GitHub instead of registry | @ omitted from name | Use @scope/name |
| Cannot publish to org scope | Not a member or no write access | Join org, request team access |
| Scope associated with wrong registry | .npmrc precedence | Check npm config get @scope:registry |
| Private package viewable by others | Access level not set correctly | npm 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
| Item | Value |
|---|---|
| Scoped name format | @scope/package-name |
| Installation path | node_modules/@scope/package-name |
| Default visibility | Private (restricted) |
| Public publish flag | --access public (first publish) |
| Free account + scoped + no flag | HTTP 402 |
| Scope to registry | @scope:registry in .npmrc |
| Org scope publish | Must be org member with write access |
| Access change after publish | npm 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 publicis 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:registrykey in.npmrcdirects 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/packagerequires being a member of theorgorganization 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 orpackage.jsondependency 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.
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!