unpkg: five packages, two clouds, one URL shape
The CDN for everything on npm
At a glance
- What is it?
- unpkg serves files out of npm packages over a URL with three parameters, and this repository is its production source: a website and file worker, a package browser app, an ESM import worker, a Bun backend that untars packages, and a shared TypeScript library. Running it means four local services on four ports, and deploying it means Fly for one piece and Cloudflare for three.
- Who is it for?
- unpkg fits a project that wants a pinned file from npm without a bundler step, and the URL shape has not changed in the way that matters, since the version segment takes a range rather than requiring an exact version. Two things to weigh before you rely on it as infrastructure.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 43 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One URL shape, and the version segment takes a range
The entire public interface is one pattern:
https://unpkg.com/:package@:version/:fileThree parameters. The first is the package name, the second is the version, and the third is the path to a file inside that package.
The detail worth pausing on is the second one. The readme calls it a version range rather than an exact version, which means a URL written with a range does not identify one immutable file: something between the client and npm decides which version satisfies it, and that decision can change under a URL that never changed.
For a documentation page loading a library from a CDN that is exactly the behaviour you want, since it gets updates without a redeploy. For a production bundle it is a supply chain decision you should make deliberately, by resolving the range to an exact version and keeping that URL.
Everything else in the repository is machinery behind those three parameters.
Five packages, and only one of them stores files
The repository is described as the production source, and it holds five packages in a workspace.
The website package is the main site and also the worker that serves package files over the CDN. A second package is the package browser app, the page where you look through a package's contents. A third is the ESM worker behind a separate hostname, which handles browser-ready module output, CSS modules, import maps, and inline transforms for TypeScript and its JSX flavour. A fourth is the file server backend, and it is the only package that touches npm directly: it fetches tarballs, extracts files, and builds the module artifacts the workers serve. The fifth is a shared TypeScript library used by the workers and by that backend.
So the division of labour is: one Bun service that knows about npm, three edge workers that answer requests, and one library they all share.
The separation is worth noting because it is what makes the edge side cheap. The workers do not talk to npm; they read from a backend that has already done the fetching and extraction. Everything the CDN serves was prepared by the one process that can fail slowly.
Locally it is four services on four ports
The development instructions require two tools before anything else: pnpm for workspace tooling, and Bun for the runtime and the tests.
After installing those, the sequence is short. Install dependencies, run the tests, and then start servers:
pnpm --filter unpkg-files dev
pnpm --filter unpkg-www dev
pnpm --filter unpkg-www dev:assets
pnpm --filter unpkg-app dev
pnpm --filter unpkg-app dev:assets
pnpm --filter unpkg-esm devSix commands for four services, because the two HTML applications each need a second process for their assets. The ports are assigned in the same order as the packages: the website on port 3000, the browser app on 3001, the module worker on 3002, and the file backend on 4000.
That last number is the one to note. The backend sits on a different port from everything else because it is the piece that does the slow work, and locally it is the process you will wait on when you request a package for the first time.
There is no single command that starts everything, so the first local run is six terminals or one script you write yourself.
The test entry point builds the whole workspace first
The root manifest has a test script and a script that runs before it, and the order matters more than it looks.
The pretest hook runs the recursive build across every package, and the test script then runs the per-package tests. So a plain test run compiles the entire workspace before it tests anything, which is the safe arrangement for a monorepo where a worker's tests may depend on artifacts another package produced.
The cost is that a full test run is a full build, and there is no documented way to skip the first half. For a five-package repository with a Bun backend and three edge workers, that is the difference between a fast inner loop and a slow one, and the manifest does not offer a shortcut.
Three of the module worker's checks sit outside the package test scripts, as separate scripts pointing at individual files under the scripts directory: a browser smoke test, a readiness test for the beta, and a compatibility suite.
The ESM output is compared against another CDN
The compatibility suite is the most interesting script in the manifest, because of how it is configured.
There are two variants. One runs the suite against a default origin. The other sets an environment variable to a local address on port 8081 and runs the same suite, which is described as the local baseline.
The variable name is the clue to what the comparison is against: an origin for a service whose name is spelled differently. There is also a script that vendors a baseline by invoking a shell file in a subdirectory of the scripts folder.
So the module worker's output is not only tested for correctness, it is compared with what another module CDN produces for the same request. That is a defensible way to hold a transformation to an external reference, and it is also a dependency on someone else's behaviour, which means the suite's expectations and that service's output can drift apart in a way nobody in this repository controls.
The worker itself is doing the work worth testing: turning a package file into browser-ready modules, handling CSS modules, emitting import maps, and transforming TypeScript and its JSX inline.
Deployment splits one package to Fly and three to Cloudflare
The two-cloud split is visible in the instructions, and each half has its own configuration file.
The file backend goes to Fly.io, and the first thing the readme asks for is an account. Then you edit the configuration inside that package and put in your own application name, and run the deploy script for that package. Nothing else is needed for that half.
The three workers go to Cloudflare, and they need two edits each. The first is the routes entry in the worker's own configuration file, which you point at your own domains. The second is the environment variables in the same file, which exist so the workers can find one another in production. That second point is the interesting one: service discovery here is configuration, not discovery, so a worker that cannot resolve its neighbours fails at request time rather than at deploy time.
Authentication is one API token. You copy the example environment file to a local file with a different name, set the token in it, and the deploy scripts load that file automatically before invoking the deployment tool. The readme is specific about the permissions: worker deploy permissions for deploys, and cache purge permissions when purging CDN entries.
There is an all-in-one command that deploys the three workers in sequence, a staging variant that uses each worker's staging target, and per-package deploy commands if you would rather go one at a time.
A Bun floor in engines, and a Node one-liner in a script
The root manifest declares one runtime requirement and then uses two.
The declared engine is Bun, at a specific minimum version, and the package manager is pinned exactly. Every script that actually does work runs through one of those two: the build recurses through the workspace, the asset build runs a TypeScript file with Bun, and the three module tests are Bun scripts too.
Then there is the clean script for reports, which runs Node with an inline expression that removes a directory and recreates it empty. No Node version is declared anywhere in the manifest.
That is harmless in practice, since anything that can run Bun can usually run Node, and the operation is trivial. It is worth knowing anyway, because it means the manifest is not a complete statement of what the toolchain needs: the floor it declares covers the runtime that matters and says nothing about the one that appears in a cleanup command.
The other cleaning script is worth a glance for a different reason. The clean target shells out to git to remove ignored files across the whole tree, which means a clean is as destructive as git's own idea of ignored files in that checkout.
The example environment file is one empty line
The repository has an example environment file, and it contains a single line: the Cloudflare API token name, with nothing after the equals sign.
There is no comment, no placeholder value and no second variable. That is a small file, and it tells you something about how this project handles configuration: everything that varies between deployments is either in a per-worker configuration file or in the platform's own environment settings, and the only thing a developer needs locally is one credential.
The readme reinforces it. The workers find each other through variables in their own configuration files, the Fly application name is edited in that package's configuration, and the API token is the only value the local environment file is for.
So the local development story has no database, no cache and no signing secret to configure. Everything a request needs at runtime is either public information about a package on npm or a URL your workers already know.
The remaining root entries are the workspace tooling: a package manifest, a workspace definition, a lockfile, an npm configuration file, a formatter configuration, the licence, and two directories of agent instruction files alongside a top-level instruction document.
Editorial conclusion
unpkg fits a project that wants a pinned file from npm without a bundler step, and the URL shape has not changed in the way that matters, since the version segment takes a range rather than requiring an exact version. Two things to weigh before you rely on it as infrastructure. There are no tagged releases in this repository, so there is nothing to pin except the commit you cloned. And the workers find each other through configuration variables set per worker, so a deployment is three configuration files that have to agree with each other, plus one API token kept in a local environment file. Anyone who needs a specific immutable artifact in production should resolve the version range once and pin the exact file URL, because that is the part the system does not promise to keep stable.
Frequently asked questions
What is unpkg.com used for?
For loading any file from npm over a CDN, using a URL of the form https://unpkg.com/:package@:version/:file, where the version segment is a version range and the file segment is a path to a file inside the package.
how to use unpkg
By building a URL in the documented shape, with a package name, a version range and a file path. This repository documents no HTML snippet or script tag example and points to unpkg.com itself for further usage details; the rest of the readme covers running and deploying the service rather than consuming it.
What is unpkg.com?
A global content delivery network for everything published to npm. This repository holds its production source as five packages: the website and file serving worker, a package browser app worker, an ESM import worker for a separate hostname, a Bun file server backend that fetches and extracts npm tarballs, and a shared TypeScript library.
What does it take to run unpkg locally?
pnpm and Bun installed first, then pnpm install and pnpm test. Six dev commands start the file backend, three workers and the asset servers for the two HTML apps, listening on ports 3000, 3001, 3002 and 4000.
How is unpkg deployed?
The file backend is deployed to Fly.io after setting your own app name in that package's config. The three workers go to Cloudflare, each needing its own routes and environment variables edited in its configuration file so the workers can find one another, and a Cloudflare API token read from a local .env.local copied from the example file.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/unpkg-unpkg)