Elements vs pnpm

Markdown

pnpm is an open-source package manager for JavaScript, MIT licensed and hosted on GitHub. A package manager downloads the npm packages a project declares and puts them where Node.js can load them. pnpm calls itself a "fast, disk space efficient package manager" (pnpm). It keeps one content-addressable store on each machine, and "when packages are installed, their files are hard-linked from that single place, consuming no additional disk space" (motivation). Inside a project, it builds node_modules out of links: each package lives in a hidden node_modules/.pnpm folder, and "pnpm uses symlinks to add only the direct dependencies of the project into the root of the modules directory" (symlinked node_modules structure). It records resolved versions in pnpm-lock.yaml (Git), and it has "built-in support for monorepositories" through workspaces declared in pnpm-workspace.yaml (workspaces). pnpm is free, and it is funded by sponsors through Open Collective.

Elements plays in this category, and package installing is one job of a larger system. Elements is an integrated app environment for building web apps, built for people and their agents. At its center is a project server that runs while you work: it holds the build graph and build state in memory, builds in milliseconds and answers agents and humans in microseconds. The same system comes with a full package installer for the npm registry, a test runner, a bundled Postgres database, a deploy command for any Ubuntu server you reach over SSH, and a default framework that just works (packages, start).

pnpm installs packages, and then its job is over. Type checking, tests, bundling, the app server, the database, migrations and deploy are each another tool, with its own config and its own command, and every seam between them is a place an app breaks, for a person or an agent. pnpm adds a seam of its own: a node_modules made of symlinks, which some tooling does not work well with. The Elements installer writes a real node_modules tree, and because it is part of the project server, every install lands in the same build that type checks your code, runs your tests and applies your migrations. The apps it builds stay close to the metal, HTML, CSS, TypeScript, SQL and function calls, with a framework vocabulary of app.route(), session.login(), sql(), @rpc and tests.

At a Glance

Elements pnpm
What it is An integrated app environment built around a project server, with a full package installer, a default framework, Postgres, tests and deploy A package manager, run as a command
After an install The app rebuilds against the new packages: type checking, tests, migrations and program analysis in one build The command exits; checking the code is another tool's job
node_modules A real tree laid out the way Node resolves modules, so anything that reads node_modules works Symlinks into a hidden .pnpm folder by default, with a hoisted mode for tooling that "doesn't work well with symlinks" (settings)
Machine store One cache per machine under ~/elements/packages, shared by every project One content-addressable store per machine, hard-linked into projects
Placing files Only packages that changed are written, cloned copy-on-write where the filesystem supports it Every package linked from the store, by hard link or clone, falling back to copies
Config config.jsoc, and a generated package.lock package.json, pnpm-workspace.yaml, .npmrc for auth and registries, and pnpm-lock.yaml
Shared code Local packages found on a search path, watched and deployed with the app A monorepo workspace with the workspace: protocol, or pnpm link
Running tasks One build that redoes only the work a change touched pnpm run and pnpm -r run run package scripts
Deploy elements deploy to any Ubuntu server over SSH pnpm deploy copies one workspace package into a folder you take to a server
Funding Sells the tooling directly, per machine, with nothing metered (pricing) Sponsors, through Open Collective

A node_modules Every Tool Can Read

Node.js finds a package by walking up the folders from the file that imports it and looking in each node_modules. Most tools that read packages, from bundlers to test runners to serverless packagers, are written to that layout.

pnpm builds a different one. Every file of every package is "a hard link to the content-addressable store," and the packages themselves sit in node_modules/.pnpm, connected to one another by symlinks (symlinked node_modules structure). A package with peer dependencies is linked again for each set of peers it resolves to: pnpm "has to hard link foo@1.0.0 as many times as there are different dependency sets" (how peers are resolved).

Not every tool follows that structure, and pnpm's docs say so. Its nodeLinker setting has a hoisted mode that creates "a flat node_modules without symlinks," and the docs list when to reach for it: "Your tooling doesn't work well with symlinks," serverless platforms such as AWS Lambda that "don't support symlinks," publishing with bundledDependencies, and running Node with --preserve-symlinks (settings). The FAQ entry "pnpm does not work with <YOUR-PROJECT-HERE>?" gives the same answer: a dependency imports a package it never declared, and the fix is the hoisted layout (FAQ). On Windows, the FAQ says, "using symbolic linking on Windows can sometimes be problematic, however, pnpm has a workaround" (FAQ). Each of those is a decision you make about your package manager before your app works.

In Elements, node_modules is a real tree, laid out the way Node resolves modules, with nested copies where two packages need different versions of the same dependency. Anything that reads node_modules works, and every npm package installs as it is (packages). There is no layout to choose and no mode to switch to when a tool does not cooperate.

Installs Feed Straight Into the Build

pnpm is a fast package installer, and the Elements package installer is just as fast. But Elements does more: its installer is a fully functional package installer, and it is also part of the project server, so every install feeds the build.

Run elements install stripe, or add a line to the dependency map in config.jsoc and save. The project server installs what changed, then rebuilds your app against the new packages. Package code is built like your own code: checked, tree-shaken and hot reloaded, and every import path is resolved at build time to the exact file it will load at runtime (packages). If the new version of a package changed a function your code calls, the type error, the failing test and every other build error come back in that build, from elements build -json, in the terminal and in the editor (build).

The installer runs on the project server alongside builds and test runs and never at the same time as one, so two installs never conflict and an install never lands in the middle of a build. An install that cannot resolve prints the reason and exits non-zero, and neither config.jsoc nor package.lock is touched (packages).

pnpm install is a command that writes node_modules and exits (pnpm install). What the new packages did to your code shows up when you next run the type checker and the test runner, each of them another program.

Elements Writes Only What Changed, as Real Files

pnpm links each package's files from the store, by hard link or by clone, and falls back to copying where neither is possible; a store on another drive or filesystem means "packages will be copied, not linked" (FAQ, settings).

The Elements installer keeps its cache of registry manifests and package sources under ~/elements/packages. It resolves from the manifest rather than the tarball, so a fresh install waits only on bytes it has not seen before. It computes the new node_modules tree in memory and compares it with the tree on disk, so it writes only the packages that changed, and an install that changes nothing touches nothing. A package that is written is cloned into place, copy-on-write where the filesystem supports it (packages). On such a filesystem, a cloned file shares its bytes on disk with its source until one of them changes, and the project still holds real files.

Shared Code Without a Monorepo

Many teams share code between apps as packages they have not published: a component library, an API client, a set of utilities.

pnpm's answer is a monorepo. "A workspace must have a pnpm-workspace.yaml file in its root," which lists the package folders, and each app depends on a sibling package through the workspace: protocol, under which "pnpm will refuse to resolve to anything other than a local workspace package" (workspaces). From there come catalogs to keep versions aligned across packages (pnpm-workspace.yaml), a selector syntax to "restrict commands to specific subsets of packages" (filtering), and pnpm -r run to run each package's scripts in dependency order (pnpm run). The other route, pnpm link, symlinks a folder into one project, and "pnpm will not install the dependencies of the linked package, you will have to install them manually" (pnpm link).

In pnpm, the shared package and the app sit in one repository:

pnpm-workspace.yamlpnpm
packages: - "apps/*" - "packages/*"
apps/web/package.jsonpnpm
{ "dependencies": { "@acme/ui": "workspace:*" } }

In Elements, the app names the package and gives it the version local:

config.jsocElements
package: { dependencies: { "@elements/app": "latest", "@acme/ui": "local", }, },

The project server finds @acme/ui on the package search path, the directories in ELEMENTS_PACKAGE_PATH, and installs it like any other package, its dependencies included. Local packages are watched: save a file in the package and the app rebuilds against it, so you work on the package and the app together in one editor with no linking step. Publish the package and switch the version to a registry release, and the local copy is removed on the next install. A deploy ships local packages to the server and installs them there, so an app built on an unpublished package deploys like any other (packages). The package and the app can live in separate repositories or the same parent folder, and neither needs a workspace file.

A Package Manager Leaves the Rest to Other Tools

A package manager answers one question: which files go in node_modules. A web app needs more answered before it works: whether the code type checks, whether the tests pass, whether the database schema matches, and how the app reaches a server.

pnpm runs scripts you write for each of those. pnpm run "runs a script defined in the package's manifest file" (pnpm run), and each script calls another tool: tsc for types, a test runner, a bundler, a migration library. In a workspace, pnpm -r run runs a script in every package, ordered by dependencies. For deploy, pnpm deploy copies one workspace package and its dependencies into a target folder, "a portable package that can be copied to a server" (pnpm deploy); the server, the process manager, TLS and the database are still yours to set up.

terminalpnpm
pnpm install # packages pnpm exec tsc --noEmit --watch # type checking pnpm test # whichever test runner the script calls pnpm -r run build # every workspace package's build script pnpm deploy --filter=web ./out # a folder to copy to a server
terminalElements
elements install stripe # installs, then rebuilds the app against stripe elements build -json # every build error, from memory elements deploy # the same build, live on your servers

In Elements, one build covers installing, type checking, compiling, migrations, tests and release, and the project server redoes only the work a change touched (build). Each test runs in a Postgres transaction that rolls back when it ends, and a build with any error never reaches the running app (tests). elements deploy provisions any Ubuntu server you reach over SSH, sets up Postgres and a load balancer with Let's Encrypt certificates, runs the tests again on the server and goes live with an atomic swap (deploy). The Postgres on the server suits a prototype or a single-server app; for several servers, point DB_HOST at a Postgres they share, your own or a managed provider's.

For an Agent, Each Dependency Is a Question

pnpm's design is built around projects with hundreds of dependencies, each copied into every node_modules on the machine (motivation). More app code is now written by AI coding agents such as Claude Code and Codex, and for an agent the cost of a long dependency list is not disk space. A package earns its place when it does something an agent cannot write quickly from standards, such as a payment client or an image library. An agent does not need a package for what a few lines of TypeScript do, and it would rather write a page in HTML and CSS than learn a UI library. Every dependency past that point is another API for the agent to look up, another version to keep compatible, and another place for its code to go wrong.

Some framework is plumbing every app needs, and Elements ships that plumbing: routing, typed server calls with @rpc, sessions, sql() and the database driver. Past the plumbing, each further abstraction costs an agent more to learn than it saves, so Elements stops there and puts its engineering into correction: telling the agent what it got wrong the moment it gets it wrong. When an agent adds a package, elements build -json reports what the package broke in the same answer as every other build error, because the install and the build are one system. Your terminal, your editor and every agent working in parallel read that one build state.

Who Pays for pnpm

pnpm is a sponsor-funded open-source project. It raises money through Open Collective, and its homepage lists the companies that sponsor it (pnpm). Sponsorship is renewed or not by each sponsor, so the work that maintains a project funded this way rises and falls with what sponsors choose to give, and a package manager sits under every install your app depends on.

Elements sells its tooling directly, per machine, with nothing metered (pricing). It charges for the tooling, not the framework: the app framework and runtime packages such as @elements/app are MIT licensed, and everything you build is yours (licenses).

Moving from pnpm

An agent can move a pnpm project to Elements easily. The dependencies in package.json become the dependency map in config.jsoc, elements install resolves them from the same npm registry into a real node_modules tree, and packages shared through a workspace become local packages (packages). You keep a fast package installer, and you gain a build that checks every install, along with the default framework, Postgres and deploy (start).

Questions

What is the difference between pnpm and Elements?

pnpm is a package manager: it installs npm packages from a shared store into node_modules, using hard links and symlinks. Elements is an integrated app environment built around a project server. It comes with a full package installer for the npm registry, and every install feeds the same build that type checks your code, runs your tests and applies your migrations. It also comes with a default framework, a bundled Postgres database and a deploy command.

Does Elements use symlinks in node_modules like pnpm?

No. Elements writes a real node_modules tree laid out the way Node resolves modules, with nested copies where two packages need different versions of the same dependency, so any tool that reads node_modules works. pnpm links packages from a hidden .pnpm folder by default, and its docs describe a hoisted mode for tooling that does not work well with symlinks (pnpm settings).

Does Elements share packages between projects like the pnpm store?

Yes. Elements keeps one cache of package manifests and sources per machine under ~/elements/packages, shared by every project, so a package you have installed once lands in the next project without a download. Packages are cloned into node_modules, copy-on-write where the filesystem supports it, and an install writes only the packages that changed.

Does Elements support pnpm workspaces or monorepos?

Elements has local packages instead. Give a dependency the version local in config.jsoc, and the project server finds it on the package search path, installs it with its dependencies, watches it, and rebuilds the app when you save a file in it. A deploy ships local packages with the app. The package and the app can live in separate repositories, with no workspace file.

Can I keep using pnpm with an Elements app?

There is no need to. Dependencies live in config.jsoc and resolve into package.lock, and the Elements installer installs them from the npm registry as part of the build, so every install is checked against your code in the same build.

Who funds pnpm?

pnpm is a sponsor-funded open-source project. It raises money through Open Collective, and its homepage lists its corporate sponsors (pnpm on Open Collective). Elements sells its tooling directly, per machine, and its app framework and runtime packages are MIT licensed.

Can I move a pnpm project to Elements?

Yes, easily. An agent moves the dependencies from package.json into config.jsoc, and elements install resolves them from the same npm registry. Packages shared through a pnpm workspace become local packages on the package search path.