deno
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
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:
deno installDeno expands ${VLT_TOKEN} from the environment, so this file
contains no secrets and is safe to commit.
Install from the npm mirror
deno install npm:abbrevEvery bare npm: specifier resolves through your account mirror,
including transitive dependencies. To move an existing project onto
the mirror:
rm -rf deno.lock node_modulesdeno installInstall from your main registry
deno install npm:@acme/my-packageThen 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 newerthan 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:
deno install --min-dep-age=0For 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=0or 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:
echo 'VLT_TOKEN=<your token>' >> .envecho '.env' >> .gitignoreDeno does not read .env automatically — pass --env-file so the
variable is available for .npmrc expansion:
deno install --env-filePublishing
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:
deno packnpm publish acme-my-package-1.0.0.tgzdeno 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_TOKENis 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 aserror: 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. Deletedeno.lockwhen 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 indeno.json.deno.jsonhas no registry or token field, so the two halves of this setup live in different files. - In a monorepo, deno looks for
.npmrcnext to thedeno.jsonit loaded and then upward from there — not from the directory you happened to run the command in. Keep.npmrcbesidedeno.jsonat the workspace root.