Skip to content

Verified against deno 2.9.

Deno reads npm registry configuration from .npmrc, so setup is the same shape as npm. Deno installs from both registries. It can also build a publishable npm package from a Deno project with deno pack, though the upload itself runs through npm publish — see publishing.

Quick start

Terminal window
export VLT_TOKEN=<your token>

Create .npmrc in your project root:

registry=https://registry.vlt.io/acme/npm/
@acme:registry=https://registry.vlt.io/acme/main/
//registry.vlt.io/acme/npm/:_authToken=${VLT_TOKEN}
//registry.vlt.io/acme/main/:_authToken=${VLT_TOKEN}

Then install as usual:

Terminal window
deno install

Deno expands ${VLT_TOKEN} from the environment, so this file contains no secrets and is safe to commit.

Install from the npm mirror

Terminal window
deno install npm:abbrev

Every bare npm: specifier resolves through your account mirror, including transitive dependencies. To move an existing project onto the mirror:

Terminal window
rm -rf deno.lock node_modules
deno install

Install from your main registry

Terminal window
deno install npm:@acme/my-package

Then import it as normal:

import myPackage from 'npm:@acme/my-package'

Public packages

Public packages on main install without a token — the registry line alone is enough, with no auth line:

@acme:registry=https://registry.vlt.io/acme/main/

Brand-new versions are hidden for 24 hours

Like Yarn, Deno 2.9 ships a supply-chain protection that skips very recent releases — the minimum dependency age, on by default with a 24-hour window. It applies to custom registries too, so right after publishing to main an install fails:

error: Could not find npm package '@acme/my-package' matching '*'.
A newer matching version was found, but it was not used because it was newer
than the specified minimum dependency date of 2026-09-09 06:54:51 UTC.

Deno names the policy and the flags in its hint text, so this one is easy to diagnose. To unblock a one-off install:

Terminal window
deno install --min-dep-age=0

For a permanent fix, exempt your own scope while keeping the protection for everything else, in deno.json:

{
"minimumDependencyAge": {
"age": "P1D",
"exclude": ["npm:@acme/*"],
},
}

The npm: prefix in exclude is required. "@acme/*" on its own silently fails to match and the install stays blocked.

To switch the gate off entirely instead, either put this in .npmrc:

min-release-age=0

or set NPM_CONFIG_MIN_RELEASE_AGE=0 in the environment, which is the more convenient form in CI.

Loading the token from a .env file

If you keep the token in a file rather than your shell profile:

Terminal window
echo 'VLT_TOKEN=<your token>' >> .env
echo '.env' >> .gitignore

Deno does not read .env automatically — pass --env-file so the variable is available for .npmrc expansion:

Terminal window
deno install --env-file

Publishing

deno publish targets JSR only — it has no --registry flag and cannot upload to main. But deno pack builds a real npm package from a Deno project, so publishing to main takes two steps: pack with deno, upload with npm.

This works from a Deno-native project with no package.json at all. Given a deno.json like this:

{
"name": "@acme/my-package",
"version": "1.0.0",
"exports": "./mod.ts",
"license": "MIT",
"publish": { "include": ["mod.ts", "README.md", "LICENSE"] }
}

pack and publish:

Terminal window
deno pack
npm publish acme-my-package-1.0.0.tgz

deno pack transpiles your TypeScript and writes a tarball containing generated .js, .d.ts and a generated package.json with correct exports, main and types fields. The result is an ordinary npm package: verified end-to-end by publishing to main and then consuming it from both deno run and plain Node.

The npm publish step needs the .npmrc from the top of this guide so the scope resolves to main. Useful deno pack flags: --set-version <v> to override the version, --dry-run to preview, and -o to name the output file.

bun publish <tarball> can also upload a prebuilt tarball, but it aborts with MissingPackageJSON in a Deno-only project, so use npm unless you keep a package.json too.

Gotchas

  • Use ${VAR}, not $VAR. Only the braced form is expanded.
  • An unset variable is left as literal text, not emptied. A missing VLT_TOKEN is sent as the literal string ${VLT_TOKEN}, so a forgotten CI secret produces a bogus credential rather than a clear “no token” error. Use ${VLT_TOKEN?} if you want an empty value instead.
  • Most registry failures report was not found. A refused connection, a 404 and a bad token all surface as error: npm:<pkg> was not found., so check the registry URL and the token before concluding the package is missing. The age gate is the exception — it produces its own clearly-labelled error.
  • Switching registries requires regenerating deno.lock. Unlike installs from npmjs, a custom-registry install records the full tarball URL in the lockfile. A stale lockfile will keep fetching tarballs from the old host — silently, and unauthenticated if you no longer have credentials for it. Delete deno.lock when you change registries.
  • Trailing slashes must match between the registry URL and the auth line.
  • A host-wide //registry.vlt.io/:_authToken=${VLT_TOKEN} also works in deno and covers both registries, but the explicit per-registry lines above stay portable if you later add bun to the project.
  • Registry and auth settings go in .npmrc; the age gate goes in deno.json. deno.json has no registry or token field, so the two halves of this setup live in different files.
  • In a monorepo, deno looks for .npmrc next to the deno.json it loaded and then upward from there — not from the directory you happened to run the command in. Keep .npmrc beside deno.json at the workspace root.