# Content-addressable store (/docs/packages/content-addressable-store)



Every package LPM CLI downloads goes into a **single global store** at `~/.lpm/store/`. Every project that depends on the same package shares the same on-disk copy — and not just the raw bytes, but the materialized wrapper directory with sibling symlinks already laid out. The result: installing the same dep set across 50 projects costs disk-space and time once, not 50 times.

This page covers the design — what the store is, how packages get into it, how projects reach into it from `node_modules/`, and the maintenance surface.

## On-disk layout [#on-disk-layout]

```text
~/.lpm/store/
└── v2/
    ├── objects/                            ← extracted package bytes
    │   ├── sha512-aabb…/                   ← one entry per content hash
    │   │   ├── package.json
    │   │   ├── index.js
    │   │   └── …
    │   └── …
    ├── links/                              ← per-graph wrapper directories
    │   ├── react@19.2.4+a1b2c3d4/
    │   │   ├── node_modules/
    │   │   │   ├── react/                  ← link from objects/<sri>
    │   │   │   ├── scheduler/              ← symlink to a sibling link entry
    │   │   │   └── …
    │   │   └── .lpm-link-meta.json         ← sidecar (object SRI, dep targets)
    │   └── …
    └── compat/                             ← macOS cached compatibility islands
        ├── <island-key>/
        │   ├── <graph-key>/node_modules/<pkg>/
        │   └── .lpm-island-complete
        └── …
```

Three arms under `v2/`:

* **`objects/<sri>/`** — content-addressable extracted bytes. One entry per unique tarball content; shared by every package version with that hash. The SRI is the same one verified against the lockfile.
* **`links/<graph-key>/`** — per-graph wrapper directories. The graph-key folds in the package's name, version, platform tuple, **linker mode** (hoisted vs isolated produces distinct keys), peer-context (empty in hoisted mode), dep edges, alias edges, root-link names, source disambiguator (Registry vs Tarball/Git), and **patch fingerprint** (a `lpm patch`'d install gets its own link directory and never shares bytes with the unpatched coords). Two installations of the same `(name, version)` are interchangeable iff every one of those inputs matches — so projects with compatible graphs share the same link entry, while a hoisted vs isolated install of the same dep set produce two link entries.
* **`compat/<island-key>/`** — macOS-only cached compatibility islands for framework/tooling bins that need a project-local dependency island. The island key includes each entry's graph key, source SRI, and current link-entry content digest, so stable-SRI local sources get a fresh island when their bytes change.

Each link entry's `node_modules/<pkg>/` is a clonefile (macOS) / reflink-or-hardlink (Linux) of the corresponding `objects/<sri>/` directory. Sibling entries (`scheduler/` next to `react/` above) are **symlinks** pointing at other graph-keys' link entries — not copies. This keeps the inode budget small even on a machine with thousands of packages.

## How packages enter the store [#how-packages-enter-the-store]

`lpm install` extracts a package into the store exactly once per content hash and materializes a link entry exactly once per graph-key.

1. **Existence check (object).** If `~/.lpm/store/v2/objects/<sri>/` exists, skip extraction entirely. Content-level dedup.
2. **Extract.** Gzip-decompress + tar-walk into a staging dir under `~/.lpm/store/v2/objects/`.
3. **Security scan.** Behavioral analysis runs against the extracted source as part of the same pass. Results persist into `.lpm-security.json` next to the package files so future installs and audits don't re-scan. See [Security audit](/docs/packages/security-audit).
4. **Finalize object.** Write `.integrity` (the verified SRI) plus `.lpm-object-integrity` (the reuse-validation sidecar), then atomic-rename the staging dir into its final `objects/<sri>/` location.
5. **Existence check (link entry).** If `~/.lpm/store/v2/links/<graph-key>/` exists, skip materialization.
6. **Materialize link entry.** Clonefile/reflink the package bytes from `objects/<sri>/` into the link entry's `node_modules/<pkg>/`. Write sibling-dep symlinks for the package's declared deps. Write the `.lpm-link-meta.json` sidecar.
7. **Finalize link entry.** Atomic-rename the staging dir into its final `links/<graph-key>/` location.

The atomic renames are the visibility boundary. A concurrent install racing for the same content or graph-key either sees the entry or doesn't — never a partially-extracted state.

## Store integrity mode [#store-integrity-mode]

LPM CLI validates an existing `objects/<sri>/` directory before reusing it. The persistent mode lives in `~/.lpm/config.toml > integrity` and is managed by [`lpm config integrity`](/docs/infra/config#store-integrity):

```bash
lpm config integrity --set source   # default
lpm config integrity --set tree     # stricter, slower
```

`source` is the default. It checks the object's source identity sidecar against the expected source SRI without rehashing every expanded file, so warm installs stay fast.

`tree` rehashes the expanded object tree before reuse and records a `.lpm-tree-snapshot.json` metadata fast path for later checks. Use it when detecting local tampering or corruption inside `~/.lpm/store/v2/objects/` matters more than warm-install speed.

This is separate from `lpm install --strict-integrity`: that flag controls whether tarball URL dependencies must declare an inline SRI before first use. `integrity = "tree"` controls how already-expanded v2 store objects are validated before reuse.

## How projects reach into the store [#how-projects-reach-into-the-store]

Project `node_modules/<pkg>/` is a **symlink** straight into a global link entry:

```text
<project>/node_modules/react
  → ~/.lpm/store/v2/links/react@19.2.4+a1b2c3d4/node_modules/react

<project>/node_modules/.bin/
  └── (real dir; bin shims are project-local)
```

When Node resolves `require("scheduler")` from inside `react/`, it walks up to the link entry's `node_modules/` and finds `scheduler/` as a sibling symlink — pointing at *another* link entry that scheduler is materialized under. Same Node-resolver semantics as a flat or pnpm-isolated layout, just with the wrapper tree relocated to a global, shared location.

| OS                            | Object → link-entry mechanism                                                 |
| ----------------------------- | ----------------------------------------------------------------------------- |
| macOS (APFS)                  | `clonefile(2)` — copy-on-write reflinks. Free until something writes.         |
| Linux (Btrfs, XFS, OverlayFS) | reflink via `ioctl(FICLONERANGE)` when the FS supports it; hardlink fallback. |
| Linux (ext4)                  | hardlink.                                                                     |
| Linux (other)                 | hardlink.                                                                     |
| Windows                       | hardlink.                                                                     |

Project `node_modules/<pkg>` symlinks themselves are real symlinks on every platform (Windows uses directory symlinks / junctions).

## Cross-project sharing [#cross-project-sharing]

Because link entries are keyed by graph context — not by project — every project that resolves the same dependency graph reuses the same on-disk wrapper directory. Five projects using `react@19.2.4` with the same peer-context cost one materialization, not five. The first install pays the materialization cost; the rest are pure symlink creation.

This survives `rm -rf node_modules`. The canonical bytes and wrapper symlinks live in `~/.lpm/store/v2/`, not in the project. A re-install only needs to recreate the project-side `node_modules/<pkg>` symlinks — which is why warm install is fast.

## Isolated vs hoisted [#isolated-vs-hoisted]

Both linker modes use the same underlying objects + link entries — they differ only in how the project's `node_modules/` is shaped:

```text
# Hoisted (default — v2 virtual-store layout)
<project>/node_modules/
  react/      → ~/.lpm/store/v2/links/react@…/node_modules/react
  …
  .bin/
    react → …
```

```text
# Isolated (--linker=isolated, auto-default for workspaces)
<project>/node_modules/
  react/  → ~/.lpm/store/v2/links/react@…/node_modules/react
  .bin/
    react → …
```

Hoisted exposes root dependencies at the project root and lets each package see its package-local dependency links inside the shared link entry. Isolated only exposes direct root deps and keeps each consumer's dependency view strict. Both reach the same canonical bytes via the global store.

## Where the store lives [#where-the-store-lives]

| Path                      | Override with                                     |
| ------------------------- | ------------------------------------------------- |
| `~/.lpm/store/` (default) | `LPM_HOME=<path>` (moves the entire LPM CLI root) |

`LPM_HOME` is useful in CI for hermetic runs — point it at a workspace-local directory and the store survives only as long as the runner does.

If `LPM_HOME` ends up on a network filesystem, LPM CLI emits a one-time warning at startup pointing at performance issues — clonefile and hardlinks degrade badly across NFS.

## Maintenance [#maintenance]

The store grows monotonically as new graph contexts appear. Two surfaces clean it up:

* [`lpm cache prune`](/docs/packages/cache) — reference-aware orphan removal. Walks `~/.lpm/known-projects.json` (the registry of every project LPM CLI has installed into), traces each project's `node_modules/` symlinks back into the store, and removes link entries + objects no longer reachable from any registered project. Cached compatibility islands are removed by crash recovery and optional LRU age. Safe to run regularly. Default is dry-run; pass `--apply` to delete.
* [`lpm store`](/docs/infra/store) — verify integrity, print the store path, blunt full wipe.

```bash
lpm cache prune                         # preview orphans
lpm cache prune --apply                 # actually remove
lpm cache prune --max-age 30d --apply   # only prune entries last touched > 30d ago

lpm store path                          # print ~/.lpm/store/
lpm store verify                        # fast structural check (pass --deep for SRI cross-check against lpm.lock)
```

If the project registry is missing, corrupt, or unreadable AND no explicit
`--project <path>` is supplied, `lpm cache prune` skips only the link/object
reachability walk (without trustworthy roots, every link entry would look
unreachable). `--apply` still performs reachability-independent crash recovery
and age eviction for compatibility islands and native build artifacts, removes
their orphaned locks, and runs the global-install tombstone sweep. The warning
names the cause; for corrupt registries it includes the parser's reason so you
can repair or delete the file. Run `lpm install` in a project to populate a
fresh registry, or pass `--project <path>` to walk a specific project.

## Concurrency [#concurrency]

The store is reads-mostly under normal operation. Three scenarios coordinate explicitly:

* **Concurrent extract for the same content.** The atomic rename + the existence check race-handle this: whichever install wins the rename publishes the entry; the other install sees the win on the next iteration and skips.
* **Concurrent install vs. `lpm cache prune`.** A writer-preference reader/writer protocol (data lock + writer-intent gate + writer-queue baton at `~/.lpm/store/.gc.lock*`) lets multiple installs run concurrently while serializing prune behind in-flight installs. See [`lpm store` — Locking model](/docs/infra/store#locking-model).
* **Concurrent prune invocations.** Serialize through the same protocol.

## Security caches [#security-caches]

After the security scan finishes, results persist alongside the package as `.lpm-security.json` inside the object directory. [`lpm audit`](/docs/packages/audit) reads from these caches; a second `lpm audit` run on an unchanged tree is essentially free. `lpm store verify --fix` refreshes stale security caches without re-extracting.

## When does this matter? [#when-does-this-matter]

* **Disk usage on a multi-project machine** — without the global link entries, every project would re-materialize its own wrapper tree. With them, the wrapper population is shared across every project with a matching graph context.
* **Warm install speed** — `rm -rf node_modules` survives the global store. Re-install is symlink creation, not extraction.
* **Cold install across projects** — the second project to need a given dep at a given graph context just makes a project-side symlink; no extraction, no clonefile.
* **Audit speed** — security analysis runs once per content hash, not per project.

## See also [#see-also]

* [`lpm cache prune`](/docs/packages/cache) — orphan removal for the global store
* [`lpm store`](/docs/infra/store) — integrity, path, blunt full wipe
* [Lockfile](/docs/packages/lockfile) — what records the integrity hash the store verifies against
* [`lpm install --linker`](/docs/packages/install) — picks isolated vs hoisted layout
* [Environment variables](/docs/reference/env-vars#filesystem-layout) — `LPM_HOME` to relocate the store
* [Security audit](/docs/packages/security-audit) — the analysis pipeline that writes `.lpm-security.json`
