How to Publish an npm Package: Setup, Trusted Publishing and Launch Steps

A step-by-step guide to getting a CLI or library onto the npm registry, with the token changes, version rules and launch work that decide whether anyone installs it.

Editorial title card reading "How to Publish an npm Package: Setup, Trusted Publishing and Launch Steps" beside the orange ZeroRaised mark.

Key takeaways

  • Publishing to npm needs a verified account with two-factor authentication, a package.json with a unique lowercase name and a version, and the npm publish command.
  • npm's scoped package guide says scoped packages such as @you/tool are published with private visibility by default, so pass the --access public flag on the first publish.
  • Run npm pack --dry-run before every first publish to see exactly which files will go into the tarball.
  • npm says direct publishing with a granular access token will be removed in January 2027, so set up trusted publishing from CI or staged publishing now.
  • A published version number can never be reused, and unpublishing is limited, so deprecate rather than delete once people depend on you.
On this page 10
  1. TL;DR
  2. This week's example: a Solana CLI published with no stored token
  3. How to publish an npm package, step by step
  4. The package.json fields that decide what users get
  5. Scoped or unscoped, public or private
  6. Tokens are on the way out: trusted publishing and staged releases
  7. Versions, dist-tags and how to undo a release
  8. The README is your listing page
  9. After you publish: launching a developer tool
  10. Where a launch listing fits once it is on npm

How to publish an npm package comes down to five moves: set up an npm account with two-factor authentication, write a package.json that names your entry points and files, check the tarball with a dry run, run npm publish, then move future releases to trusted publishing from CI. The upload takes seconds. The decisions around it (name, scope, versions, tokens and the README that becomes your listing) are what last.

Disclosure: ZeroRaised publishes this guide and appears in the final section. The steps follow the linked npm, Node.js and Hacker News documentation as checked on 18 Oct 2026, and this guide makes no promise of installs, downloads or traffic from any registry or launch site.

TL;DR#

The registry part is short. Most of the work is in package.json, in choosing between a scoped and an unscoped name, and in how future versions get published. That last part changed this year. npm's token documentation now says direct publishing of new versions with a granular access token goes away in January 2027, so a tutorial that ends by pasting an NPM_TOKEN into your CI secrets is already out of date.

For a solo maker the sensible path is a first publish by hand with 2FA, then trusted publishing from GitHub Actions for every release after that, with provenance switched on by default. Decide your version scheme before the first upload, because a version number, once used, is gone for good.

StepTool or placeWhat you needCommon snag
Accountnpmjs.com sign-upLowercase username, verified email, 2FAYour public email goes into package metadata
Describepackage.jsonname, version, license, files, exports or binUppercase or taken names are refused
Checknpm pack --dry-runnpm CLIStray files such as test fixtures or secrets in the tarball
First publishnpm publish2FA prompt on your accountScoped packages need --access public
AutomateTrusted publishing on npmjs.comGitHub Actions, GitLab.com or CircleCI cloudWorkflow filename must match exactly
Versionnpm version and dist-tagsA semver planA used version number can never be reused
Retirenpm deprecateA message for usersUnpublish is limited after 72 hours

This week's example: a Solana CLI published with no stored token#

On 17 Oct 2026, Cody Pack published "I Publish npm Packages With No Tokens Stored Anywhere" on DEV (DEV post, checked 18 Oct 2026). The post walks through how they shipped version 0.3.0 of SolAudit, an open source static analyser for Solana programs, from GitHub Actions. The opening claim is the one that matters: "The publish step involved zero secrets." And then, to be sure nobody misses it: "No npm token in GitHub Actions secrets. No token on my laptop. No token anywhere."

The npm registry record for solaudit (checked 18 Oct 2026) backs this up. Version 0.3.0 landed at 19:49 UTC on 17 Oct, and its metadata carries a provenance attestation, the signed statement npm attaches when a package is built and published from a supported CI provider. Earlier versions go back to 26 Aug 2026, with 0.2.0 on 12 Oct.

Cody Pack is candid about the cost. By their own estimate "it took me about 30 minutes to set up", and the post covers "the two gotchas that cost me a failed run each." One was the npm version on the build machine: "Trusted publishing requires a recent npm CLI (11.5.1+). The runner image ships an older one." The fix was a line in the workflow that installs the latest npm before publishing.

Two details are worth copying. First, the bootstrap: "For a brand-new package that doesn't exist on npm yet, you do one initial manual publish first", then trusted publishing takes over. Second, the release trigger is a button, not a merge: "I keep publishing manual on purpose: merging code and shipping a public package are two different decisions, and I want a deliberate click between them."

They also mark the limit plainly: "It protects the publish step, not your code." Provenance proves where a tarball came from. It does not prove that the code inside is safe.

Two smaller launches from the same week show the other end of the process. On 15 Oct, mhk sameera wrote about shipshape-cli, a preflight checker for JavaScript and React projects (DEV post, checked 18 Oct 2026): "It's on npm now as v0.1.0". The post explains the tool's own trust rule, "A tool you point at your project shouldn't be able to change it, leak it or run it", and lists that it "makes no network requests and sends no telemetry". On 12 Oct, princejain756 posted Ctxvitals as a Show HN with the install command npx ctxvitals in the title. The npm registry record for ctxvitals shows seven entries that day, from a placeholder at 11:33 UTC to 0.4.0 at 16:45 UTC.

Three makers in one week is not a pattern, and none of this predicts how their tools will do. The narrower lesson: the publish command is the easy part, and the people who wrote about it spent their words on tokens, versions and what the package does with a user's machine.

How to publish an npm package, step by step#

If you searched for how to create and publish an npm package, this is the sequence for a first release from your own machine.

1. Create the account. Sign up on npmjs.com. The account creation guide sets two rules worth knowing before you type anything: "Your username must be lower case, and can contain hyphens and numerals." Your username also becomes your scope, so pick one you would put on a tool. And the email you give is not private: "Your public email address will be added to the metadata of your packages and will be visible to anyone who downloads your packages." Then confirm it, because "You must verify your email address in order to publish packages to the registry."

2. Turn on two-factor authentication. The 2FA guide says publishing needs either 2FA on your account or a granular access token with bypass 2FA enabled. Use 2FA. A security key, or a built-in option such as Touch ID or Windows Hello, works from both the website and the command line once you set it up on the web. Save the recovery codes somewhere other than the device you use for the second factor, since "Recovery codes are the only way to ensure you can recover your account if you lose access to your second factor device." One detail catches people mid-launch: "Using a recovery code to log in triggers a temporary 72-hour security hold on your account." During that hold you cannot publish. The same page recommends linking your GitHub account so recovery is faster.

3. Write package.json. Run npm init (or npm init --scope=@your-username for a scoped package) and then edit the result by hand. The next section covers which fields matter.

4. Test it as a user would. Install the folder into another project with npm install /path/to/your-package, or pack it and install the tarball. If it is a CLI, run the command from a clean shell.

5. Check the tarball. The npm publish reference is direct: "To see what will be included in your package, run npm pack --dry-run". Read the list. Look for .env files, test fixtures, screenshots, source maps you did not mean to ship and anything with a key in it.

6. Log in and publish.

npm login
npm publish
# for a scoped package such as @you/tool
npm publish --access public

npm asks for your second factor and uploads the tarball. Your package page on npmjs.com then shows the README as its body.

7. Install it from the registry. In a fresh directory, run npx your-package --help or npm install your-package and check that the entry points resolve. This catches a build folder that was never built, or a files list that left it out.

That is the manual path. The rest is about making later releases safer than the first.

The package.json fields that decide what users get#

The package.json reference is long, but a first release only needs a handful of fields to be right.

name. "The name must be less than or equal to 214 characters." It cannot hold uppercase letters or non-URL-safe characters, and an unscoped name has to be free on the whole registry.

version. Start where your version plan says (see the versions section below). Every publish needs a new number.

license. Use an SPDX identifier such as MIT or Apache-2.0. If the code is not open source, the reference suggests UNLICENSED and also suggests setting private to true to prevent accidental publication, in which case "npm will refuse to publish it"

files. This is an allowlist of what goes into the tarball. Without it, npm packs nearly everything not ignored. With it, you ship dist/ and nothing else. Some files are always included whatever you write: package.json, the README, the LICENSE, the file in main and the files in bin. Others can never be included, among them .git, .npmrc, node_modules and lockfiles.

exports. The reference describes it as "a modern alternative to" the older main field, allowing multiple entry points and conditional resolution between environments. It also blocks access to any path you did not list, which is good for you (internal files stay internal) and a breaking change if you add it to a package people already import deep paths from. Add it before 1.0.0, not after.

bin. For a CLI, map the command name to a file: SolAudit's registry record maps solaudit to dist/cli.js, shipshape-cli's registry record maps shipshape to bin/shipshape.js. The reference asks that each bin file starts with #!/usr/bin/env node, or it runs without Node. Ctxvitals registers two names for the same file, ctxvitals and ctx-vitals, which is a cheap way to forgive a typo.

repository. Point it at the public GitHub repository. It is optional for a manual publish but required to match exactly for trusted publishing and provenance, so get it right now.

A minimal CLI manifest looks like this:

{
  "name": "@you/tool",
  "version": "0.1.0",
  "description": "Lint things",
  "license": "MIT",
  "type": "module",
  "bin": { "tool": "dist/cli.js" },
  "exports": "./dist/index.js",
  "files": ["dist"],
  "repository": { "type": "git", "url": "git+https://github.com/you/tool.git" },
  "engines": { "node": ">=22" }
}

Scoped or unscoped, public or private#

A scope is the @name/ prefix. The scopes page explains that when you sign up you are "granted a scope that matches your user or organization name", and that a scope lets you use a package name someone else already has without conflict. So @you/tool is available even if tool was taken long ago.

The catch is visibility. The scoped package guide says "By default, scoped packages are published with private visibility." "To publish a scoped package with public visibility, use npm publish --access public". The CLI v12 publish reference lists public as the default for new packages, so the pages disagree; pass the flag anyway. Later publishes keep the existing access level unless you pass a new one. You can also set publishConfig.access to public in package.json so nobody forgets.

ChoiceExampleName availabilityDefault visibilityGood for
UnscopedtoolMust be free across the whole registryPublicA CLI people will type often, if the name is free
User scope@you/toolOnly needs to be free in your scopePrivate until you pass --access publicSolo makers, related packages under one name
Organisation scope@yourco/toolOnly needs to be free in the org scopePrivate until you pass --access publicA product with several packages or several maintainers
Private package@yourco/internalSame as scopePrivateInternal code you do not want on the public registry

For a CLI, an unscoped name is easier to type.

Tokens are on the way out: trusted publishing and staged releases#

This is where most older tutorials now go wrong. The access token documentation (checked 18 Oct 2026) states: "As of November 2025, only Granular access tokens are supported." It then sets a date: "The ability to publish new package versions directly with a granular access token will be removed in January 2027." Since August 2026, tokens with bypass 2FA also cannot create or manage other tokens. If your release job reads an NPM_TOKEN secret, it has a few months left.

npm offers two replacements, and they combine.

Trusted publishing. The trusted publishing guide describes publishing from CI "using OpenID Connect (OIDC) authentication, eliminating the need for long-lived npm tokens." The CI provider vouches for the job, npm hands it a short-lived credential, and nothing reusable is stored. It needs "npm CLI version 11.5.1 or later and Node version 22.14.0 or higher", which is the gotcha Cody Pack hit. Supported runners are GitHub-hosted GitHub Actions, GitLab.com shared runners and CircleCI cloud; self-hosted runners are not supported yet.

Setup on GitHub Actions:

  1. On npmjs.com, open the package settings and add a trusted publisher. Enter the GitHub owner, repository and workflow filename (only the filename, with its .yml or .yaml extension), plus an environment name if you use one.
  2. Choose allowed actions. Configurations created after 3 Sep 2026 default to staged publishing only; tick direct npm publish if you want CI to release without a review step.
  3. Add id-token: write to the workflow permissions and run npm publish with no token at all.
  4. Publish within two days. "A newly created trusted publisher configuration must complete its first successful publish within 2 days." If it does not, it expires and you start again.
name: Publish
on:
  push:
    tags: ['v*']
permissions:
  id-token: write
  contents: read
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
          registry-url: 'https://registry.npmjs.org'
      - run: npm install -g npm@latest
      - run: npm ci
      - run: npm test
      - run: npm publish

Two warnings from the same page. First, "npm does not verify your trusted publisher configuration when you save it", so a typo only shows up as an authentication error at publish time. Second, the repository.url in package.json must exactly match the GitHub repository. And if you trigger releases with workflow_dispatch or call a reusable workflow, as SolAudit does with its manual button, read the note on which workflow name npm checks.

Once trusted publishing works, the guide recommends changing the package's publishing access to "Require two-factor authentication and disallow tokens", then revoking old automation tokens.

Staged publishing. The staged publishing page adds a review gate. CI runs npm stage publish, which uploads the version without releasing it, and a maintainer approves it later from the CLI or npmjs.com with 2FA. "Note: npm stage publish does not require 2FA." Approval does. It needs npm CLI 11.15.0 or later. When you stage a package that does not exist yet, npm creates a placeholder version, 0.0.0-stage, which explains the entries of that name at the top of the shipshape-cli and Ctxvitals registry records.

Release methodCredential stored2FA momentProvenanceStatus per npm docs
Manual npm publishNone (you log in)At publishNoFine for the first release
Granular token in CI, direct publishLong-lived tokenBypassedOnly with --provenanceDirect publishing removed January 2027
Stage-only token plus approvalLong-lived token that cannot releaseAt approvalOnly with --provenanceThe token docs' suggested migration
Trusted publishing, directNoneNoneAutomatic from public GitHub or GitLab reposRecommended
Trusted publishing, stagedNoneAt approvalAutomatic from public GitHub or GitLab reposThe guide's strongest setting

For a solo maker, trusted publishing with direct publish on tagged releases is the least friction. If you want SolAudit's deliberate click, staged publishing gives it to you without a manual trigger.

Versions, dist-tags and how to undo a release#

Learning how to release an npm package well is mostly about numbers. npm's semantic versioning page recommends "starting your package version at 1.0.0", then a patch for backward compatible fixes, a minor for backward compatible features and a major for anything that breaks. The semver specification adds: "Major version zero (0.y.z) is for initial development. Anything MAY change at any time." All three packages in this week's examples started at 0.1.0 instead, a common way to signal that the API may still move. Either works if the README says which.

npm version patch (or minor, major) bumps package.json and, per the npm version reference, "If run in a git repo, it will also create a version commit and tag." That tag is what the workflow above listens for.

Dist-tags decide what npm install gives people. The dist-tag reference explains: "Publishing a package sets the latest tag to the published version unless the --tag option is used." So a beta published without a tag becomes the default install for everyone. Publish previews with npm publish --tag next and move latest only when you mean it. Ctxvitals went from 0.1.0 to 0.4.0 in one day; fast releases like that are fine, as long as each one has a CHANGELOG line. The guide on how to write release notes covers that side.

When a release goes wrong, the unpublish policy is the rule that matters. Within 72 hours you can unpublish a new package if no other public package depends on it. After that, a package qualifies only if nothing depends on it, it "had less than 300 downloads over the last week" and it has a single owner. Whatever you remove, "Once package@version has been used, you can never use it again."

ActionCommandWhat users seeReversible?
Fix forwardnpm version patch then publishA newer version on latestYes, publish again
Move a tag backnpm dist-tag add tool@1.2.3 latestInstalls get the older versionYes
Deprecate a versionnpm deprecate tool@1.2.4 "<message>"A warning at install; the version staysYes, set an empty message
Deprecate the packagenpm deprecate tool "<message>"Warning at install, notice on the package page, dropped from npmjs.com search resultsYes
Unpublish a versionnpm unpublish tool@1.2.4Gone; installs pinned to it breakNo; the number is burned
Unpublish everythingnpm unpublish tool --forceGone; no new versions for 24 hoursNo

The deprecation guide is clear about the preference: "We strongly recommend deprecating packages or package versions instead of unpublishing them". If in doubt, fix forward and deprecate the bad version.

The README is your listing page#

There is no store page to design on npm. Your README.md is rendered as the body of the package page, and the package.json reference says the description and keywords help people find it in npm search. Treat the README like a landing page for someone skimming with a terminal open:

  • one sentence on what it does, then the install or npx command;
  • a short example with real output, as shipshape-cli's post does with its report;
  • what the package touches on the user's machine: files, network, telemetry;
  • supported Node versions and whether it is ESM, CommonJS or both;
  • a link to the repository, issues and CHANGELOG, and the licence.

That third point is the trust question. A new package from a single maintainer gives a careful developer little to go on. SolAudit answers it with provenance, shipshape-cli with a stated read-only rule. Say what yours does before anyone has to ask.

After you publish: launching a developer tool#

A package on the registry is findable, not found. As editorial judgement rather than a measured rule, the first users of a developer tool tend to arrive from places where developers already talk. All three makers this week told people within hours of publishing: Cody Pack and mhk sameera wrote DEV posts on why the tool exists, and princejain756 posted a Show HN with the one command needed to try it.

Routes that suit a CLI or library:

  • Show HN. The Show HN guidelines begin: "Show HN is for something you've made that other people can play with." An npx command is about as playable as software gets, which is why Ctxvitals put it in the title. The guidelines also rule out "blog posts, sign-up pages, newsletters, lists, and other reading material", so link the repository or the package, not an announcement. See how to write a Show HN post and Product Hunt vs Show HN.
  • A DEV or blog post on the problem, as Cody Pack and mhk sameera wrote. A post that teaches something (trusted publishing, preflight checks) travels further than one that only announces.
  • Developer-tool launch sites. DevHunt vs Product Hunt compares two common choices, and the launch sites directory lists more. If you pick a timed launch, the launch time tool helps you choose a slot.
  • The open source route, if the repository is public. How to market an open source project covers READMEs, contributors and communities.
  • Docs. Beyond one command, a quick start, an options table and examples matter more than a launch post.

Whatever the channel, answer three questions quickly: what it does, the exact install command, and what it does on the user's machine. Then reply to every issue in the first weeks, and ship fixes as patch releases with release notes.

Where a launch listing fits once it is on npm#

A published package gives you real launch material: an install command that works, a README that shows the tool in action, a repository and a version history that shows movement. None of that guarantees anyone turns up.

ZeroRaised is a free launch site for bootstrapped makers whose products have no institutional VC or accelerator equity. If your CLI, library or SDK is public and fits, you can submit it for review. The maker perks page is a separate resource for eligible makers. Neither replaces a clear README or a fast reply to the first bug report.

So, how to publish an npm package: a short command preceded by decisions about the name, the scope, the files, the version scheme and how future releases get signed off. Make those once, move releases to trusted publishing before January 2027, and the publish itself is the easy bit.

Frequently asked questions

How do I publish an npm package for the first time?

Create an npm account, verify your email and turn on two-factor authentication. Give package.json a unique lowercase name, a version, a license and the entry points, run npm pack --dry-run to check the file list, then run npm login and npm publish. Scoped packages need npm publish --access public.

What is npm trusted publishing?

Trusted publishing lets a CI workflow publish to npm with short-lived OIDC credentials instead of a stored token. You link the package to a GitHub Actions workflow, GitLab pipeline or CircleCI project in the package settings on npmjs.com, give the job permission to mint an ID token, and npm publish authenticates on its own. Public packages published this way from a public GitHub or GitLab repository get provenance automatically.

Do I still need an npm token to publish from GitHub Actions?

Not if you use trusted publishing. npm's token documentation says direct publishing of new versions with a granular access token will be removed in January 2027, and recommends a stage-only token plus maintainer approval for automation that still uses tokens.

Can I unpublish an npm package?

Within 72 hours of publishing you can unpublish if no other public package depends on it. After that, a package qualifies only if nothing depends on it, it had fewer than 300 downloads in the last week and it has a single owner. A version number can never be used again, so npm recommends deprecating instead.

Should my first version be 0.1.0 or 1.0.0?

npm's semantic versioning page recommends starting at 1.0.0. The semver specification treats 0.y.z as initial development where anything may change. Pick one, write it in the README, and bump major, minor or patch according to what each release changes.