Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Package Deprecation and Lifecycle

Publishing a package is the beginning of a commitment, and most of a package's life happens after the first release: versions that must be retired because they are broken or insecure, old major lines that reach end of support, packages that need a new name or a new owner, and eventually packages that should stop being used altogether. The npm registry is built around immutability — versions cannot be changed and are rarely removed — so every lifecycle action is a matter of signalling rather than deleting. This section covers deprecation, the narrow circumstances where unpublishing is allowed, renaming without stranding users, transferring ownership, and planning a package's end of life. The same principles apply to internal packages on private registries, where the rules are looser but the consequences for consumers are the same.

The lifecycle of a package

Stages in a package's life A package moves from active development, through maintenance of older majors, deprecation of versions or lines, possible rename or transfer, to archival. Active releases on latest Maintenance old major on an LTS tag Deprecated versions or lines flagged Renamed / moved new name or owner Archived whole package deprecated
Each stage has a registry action — tags, deprecation, ownership changes — rather than deletion.

Each stage has its own tools:

The broader release process is covered in Package Publishing & Release Engineering.

Why the registry is immutable

A published version is a promise to every consumer who has it in a lockfile: the same name and version will always resolve to the same bytes. The 2016 removal of left-pad — a tiny package that thousands of builds depended on — broke installs across the ecosystem within hours and led npm to restrict unpublishing. Since then, the registry's rules protect consumers first: versions are never reused, removal is only possible for new or unused packages, and everything else is handled by metadata that warns rather than breaks.

Lifecycle actions and their effect on consumers Compares deprecating, moving a tag, unpublishing a version, renaming and transferring ownership on reversibility and impact on existing installs. reversible breaks existing installs typical use deprecate yes no, warns broken, insecure, unsupported move dist-tag yes no rollback, channels unpublish version no yes if referenced accidental publish, secrets rename old name stays no rebrand, scoping transfer ownership yes no org adoption, handover
Deprecation and tags only warn or redirect; unpublishing is the one action that can break existing consumers.

Signals that a version, line or package should be retired

Lifecycle decisions are easier when the triggers are explicit. Typical signals:

  • A version is broken or dangerous. It crashes on import, corrupts data, ships a known vulnerability with a fix available, or was published by mistake (wrong tag, missing build output, leaked file). Deprecate that version, pointing to the fix.
  • A major line leaves support. The support window in your policy ends. Deprecate the whole line with a pointer to the migration guide, and stop backporting.
  • The package's purpose is served elsewhere. A platform API replaced it (many polyfills and utility packages), or it merged into another package. Deprecate the whole package, naming the replacement.
  • The name no longer fits. A rebrand, a move into a scope, or a split into several packages. Rename, and deprecate the old name with a pointer.
  • Nobody maintains it. Issues go unanswered and dependencies go stale. Either find new maintainers and transfer ownership, or deprecate and archive it honestly — an honest "unmaintained" message is better for users than silence.

Download statistics and dependents help size the impact. npm view <package> and the package page show weekly downloads; the dependents count on the registry shows how many published packages depend on it. A package with thousands of dependents deserves a longer notice period and more communication than one with none.

Security fixes across the lifecycle

Security is where lifecycle policy is tested. When a vulnerability is reported, the support policy decides which lines get a fixed release; the lifecycle tools decide how users are told. The sequence that works:

  1. Fix and release on every supported line (for example, 6.2.1 on latest and 5.9.4 on v5-lts).
  2. Publish a security advisory listing affected ranges and fixed versions, so npm audit reports it to consumers.
  3. Deprecate the vulnerable versions on each supported line with a message naming the fixed version, so every install of a vulnerable version prints a warning even for users who never run npm audit.
  4. For lines already out of support, add a deprecation message stating that the line is affected and unsupported, with a pointer to the upgrade guide.

The advisory reaches automated tools; the deprecation reaches humans at install time. Doing both covers users who rely on either.

Core initialisation: lifecycle metadata in the package

Some lifecycle information belongs in the package itself, where tools and users can read it:

{
  "name": "@acme/sdk",
  "version": "5.2.0",
  "repository": { "type": "git", "url": "git+https://github.com/acme/sdk.git" },
  "bugs": { "url": "https://github.com/acme/sdk/issues" },
  "engines": { "node": ">=20.19" },
  "funding": "https://github.com/sponsors/acme"
}

The repository and bugs fields tell users where to report problems and follow development — important when a package moves. engines communicates runtime support, which is part of a support policy. None of these fields can mark a package as deprecated; deprecation lives in registry metadata, set with npm deprecate, because it must be changeable without publishing a new version.

Architecture: where lifecycle state lives

Lifecycle state is split between the package tarball (immutable), the packument — the registry's metadata document for the package (mutable by owners) — and your repository and documentation:

Where each kind of lifecycle information is stored The tarball holds immutable code and manifest; the registry packument holds dist-tags, deprecation messages and owners; the repository and docs hold support policy and migration guides. 1 Tarball (immutable) code, package.json, README as published — fixed forever for that version 2 Packument (owner-editable) dist-tags, deprecation messages per version, maintainers, access 3 Repository and docs support policy, migration guides, archive notices, security advisories
Change what consumers are told through the packument; never try to change a published tarball.

Everything consumers see at install time — deprecation warnings, which version latest resolves to — comes from the packument, which is why lifecycle actions take effect immediately without a new release.

Execution strategy: a support policy

Deciding lifecycle actions case by case leads to inconsistent treatment and surprised users. A short written policy removes most decisions:

  1. Supported lines: the current major receives all fixes; the previous major receives security and critical fixes for a stated period (for example, 12 months after the next major).
  2. Deprecation triggers: a version is deprecated when it has a known security vulnerability with a fixed release available, when it was published by mistake, or when its line reaches end of support.
  3. Messages: every deprecation message says what is wrong and what to install instead.
  4. Removal: unpublishing is reserved for accidental publishes caught within the policy window and for leaked secrets.
  5. End of life: an unmaintained package is deprecated as a whole with a pointer to an alternative, its repository archived, and its README updated.

Publish the policy in the README or a SUPPORT.md, and link it from release notes when a new major ships.

Communicating lifecycle changes

Registry metadata reaches users at install time; everything else needs deliberate communication. For each lifecycle change, decide which channels to use and in what order.

Communicating a lifecycle change Announce in release notes and README, update registry metadata with deprecation or tags, notify known dependents, and archive or redirect the repository. announce release notes, README, blog registry metadata deprecate, move tags notify dependents issues or PRs upstream archive / redirect repo status, links
Announce before the registry change where possible, so the install-time warning is not the first users hear of it.

For end-of-support notices, announce at least one release cycle ahead: "4.x support ends on 2027-03-31" in the release notes of the new major, in the README and in the migration guide. For renames and merges, update the old package's README with a prominent notice in a final release, because the registry page shows the README of the latest version. For whole-package retirements with many dependents, consider opening issues or pull requests in the most-used dependent projects — the dependents list on the registry page identifies them. Finally, archive the repository (or its folder) with a notice pointing to the replacement, so people who find it through search land on the right information.

A consistent message template helps: what is changing, when, why, what to use instead, and where to ask questions. Reuse it in the deprecation message (shortened), the README notice and the release notes, so users see the same guidance wherever they encounter the change.

Internal packages on private registries

Packages on private registries follow the same lifecycle, with different rules at the edges. Most private registries — Verdaccio, Artifactory, GitHub Packages, CodeArtifact — let administrators delete versions freely, so the public registry's strict unpublish policy does not apply. That freedom is easy to misuse: deleting an internal version that other teams' lockfiles reference breaks their builds exactly as an unpublish on the public registry would. Apply the same discipline internally — deprecate first, give consumers time to move, and delete only versions that are demonstrably unused or that contain secrets. Most private registries support npm deprecate, so the same messages reach internal users at install time.

Internal packages also make ownership questions sharper, because the "users" are colleagues. Record an owning team for every internal package (CODEOWNERS for the source, registry permissions for publishing), and when a team is reorganised, move package ownership explicitly rather than letting packages become orphans that nobody may change but everybody depends on.

Measuring whether a deprecation is working

After deprecating a version or line, watch whether usage actually declines. Public packages show weekly download counts per version on the registry's package page and through its download statistics API; internal registries usually offer similar metrics or access logs. A deprecated line whose downloads stay flat for months suggests the message is not reaching users — often because the version is pulled in transitively by another popular package. In that case, contact that package's maintainers or open a pull request to update their dependency range; fixing one intermediate package can move thousands of downstream projects at once.

Security and isolation

Lifecycle operations are privileged writes. Deprecation, tag changes, ownership changes and unpublishing all require publish-level access and, for packages that require 2FA, a second factor or the trusted CI identity. Two risks deserve attention. First, ownership changes are the most sensitive operation: adding a maintainer gives them full publish rights, so ownership transfers should be deliberate, reviewed and preferably done through organisation teams — see Requiring 2FA for Package Maintainers. Second, abandoned packages are targets: an unmaintained package with a dormant maintainer account is attractive for account takeover. When you stop maintaining a package, deprecate it and remove stale maintainers rather than leaving it silently available.

CI/CD integration

Routine lifecycle actions can be automated so they happen consistently:

name: lifecycle
on:
  release:
    types: [published]
permissions:
  id-token: write
  contents: read
jobs:
  deprecate-unsupported:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: 24, registry-url: https://registry.npmjs.org }
      # When a new major ships, flag the line that just left support
      - name: Deprecate end-of-life line
        if: startsWith(github.event.release.tag_name, 'v6.0.0')
        run: npm deprecate @acme/sdk@"4.x" "4.x reached end of support; upgrade to 6.x: see MIGRATION.md"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_LIFECYCLE_TOKEN }}
      - name: Show resulting state
        run: npm view @acme/sdk dist-tags && npm view @acme/sdk@4 deprecated

Step by step: the job runs when a release is published; when that release starts a new major, it deprecates the line that the support policy retires; it prints the resulting tags and deprecation state into the log for review. Keep ownership changes and unpublishing manual — they are rare and high-impact.

Lifecycle in monorepos

Monorepos that publish many packages accumulate lifecycle work faster: packages are split, merged, renamed and retired as the architecture evolves. A few habits keep it manageable. Keep a short PACKAGES.md or a section of the README that lists every published package with its status — active, maintenance, deprecated — and the replacement for deprecated ones. When a package is retired, remove it from release configuration (Changesets fixed or linked groups, CI publish lists) in the same pull request that deprecates it, so the release tooling cannot publish it again by accident. And when a package is merged into another, publish a final version of the old package that re-exports the new one where the APIs allow, then deprecate it — the approach described in Renaming a Package Without Stranding Users.

The consumer's side: finding deprecated dependencies

Lifecycle signals only help if consumers see them. npm prints deprecation warnings during install, but in a large tree those lines scroll past unnoticed. Consumers can surface them deliberately: npm ls --all 2>&1 | grep -i deprecated in older npm versions, npm outdated for version gaps, and dependency bots that open pull requests with release notes. Renovate labels pull requests for deprecated packages and can be configured to replace them with named successors. Publishers can help by keeping deprecation messages short and actionable — they are often read in a narrow CI log — and by mentioning replacements in the package README, which remains visible on the registry page after deprecation.

Worked example: retiring a utility package

A team maintains @acme/fetch-retry, a small wrapper that added retries to fetch. Its functionality has since moved into their HTTP client package, and weekly downloads have dropped to a few hundred, almost all from their own older applications. They publish a final patch that logs a one-time warning pointing to @acme/http, update the README with a migration snippet, deprecate every version with the message "Merged into @acme/http; see the README for migration", remove the package from the monorepo's release configuration, and archive its folder into a deprecated/ directory for reference. Internal applications migrate over two sprints, tracked by a Renovate rule that flags any remaining dependency on the old package.

Pitfalls

Mistake Impact Remediation
Unpublishing a version others depend on Consumers' installs break Deprecate instead; unpublish only within policy
Deprecation message without an alternative Users see a warning but not what to do Always name the version or package to use
Renaming by publishing a new package silently Users never find the new name Deprecate the old name with a pointer
Leaving stale maintainers on old packages Account takeover risk Review owners; remove inactive ones
No written support policy Inconsistent backports and surprises Publish supported lines and windows

Guides in this topic

Every guide below solves one concrete task or error within Package Deprecation and Lifecycle. Start with the one whose symptom matches what you are seeing:

Frequently Asked Questions

Can I edit a published version to fix a typo in the README? No. Published tarballs are immutable. Publish a new patch version; the npm package page shows the README of the latest version.

Does deprecating a package remove it from search? Deprecated packages remain installable and visible; npm search ranking and the package page reflect the deprecation, and installs print the message.

What happens to a package whose owner leaves the company? If it is owned by an organisation, access follows team membership and nothing needs to move. If an individual owns it, add another owner before they leave, or transfer it to the organisation.

Should an unmaintained package be transferred rather than deprecated? If a trustworthy maintainer wants to continue it, transferring ownership can be better for users than deprecation. Vet the new maintainer carefully — package handovers have been used to introduce malicious code — and announce the transfer publicly so users can decide whether to keep trusting the package.

How long should a deprecated package stay on the registry? Indefinitely. Deprecated versions remain installable so existing lockfiles keep working; the deprecation message does the job of steering new installs elsewhere. Removal is only for the narrow cases the unpublish policy allows.

Can I undo a deprecation? Yes. Run npm deprecate again with an empty message for the same version range, which clears the warning. That is useful when a version was deprecated by mistake or a line's support is extended.

Related

Package Publishing & Release Engineering