Files
pnpm/pnpr/docs/quick-start.md
Zoltan Kochan bbae0b99fe fix(pnpr): bind authorization to immutable account and team IDs (#17824)
Use immutable account IDs for authorization and credential ownership.
Keep usernames as mutable login/display attributes, and retain deleted
accounts under generated names while allowing their original names to
be registered with fresh IDs.

Apply the same separation to teams and SCIM resources. Bind OIDC subjects
and group grants to explicit account/team IDs. Keep SCIM names as a
projection of the authoritative account store, and preserve account IDs
across password changes and htpasswd credential imports.

Store account IDs in package ownership, tombstones, staged uploads,
publication idempotency, and persistent principals. Keep registry package
and version keys as natural namespace identities.

Replace the experimental native auth format and SQL schema directly,
without migrations or compatibility aliases. Update configuration,
management APIs, bootstrap output, documentation, and registry fixtures.
Preserve joined token-owner lookups and use bulk account projections.
2026-11-07 10:54:10 +01:00

3.2 KiB

id, title
id title
quick-start Quick start

Write a config

pnpr routes packages through declared registries: every origin (a locally-hosted registry, an upstream like npmjs) is declared explicitly and claims the package names it serves, and a router resolves each name to exactly one of them. Save this as pnpr.yaml:

storage: ./storage

auth:
  users:
    file: ./users.json
    # Allow one user to self-register, for the publish step below.
    maxUsers: 1

registries:
  # Packages published to this server. The `packages:` map is this
  # registry's namespace: only these names can live here.
  local:
    type: hosted
    packages:
      '@mycompany/*':
        publish: $authenticated

  # The public npm registry. No `packages:` map — it serves every name,
  # so it must be the last source in the router below.
  npmjs:
    type: upstream
    url: https://registry.npmjs.org/
    public: true

  # A package resolves to the first source that claims its name:
  # your scope goes to the local registry, everything else to npm.
  main:
    type: router
    sources: [local, npmjs]

# The bare server URL serves the router.
defaultRegistry: main

Replace @mycompany with your own scope. There is no fall-through between registries: a @mycompany/* package that isn't published locally is a definitive 404, never a request to npmjs. This is what closes dependency confusion.

Start the server

pnpr -c ./pnpr.yaml

It listens on 127.0.0.1:7677. Running plain pnpr with no config works too. The bundled config proxies npmjs and provides a local hosted registry at /npm/~local/, whose packages need a login. It allows no self-registration, so create accounts with pnpr add-user.

Point a client at it

Configure pnpm (or npm/yarn) to use the local server as its registry:

pnpm config set registry http://127.0.0.1:7677/npm/

Now pnpm install fetches packages through pnpr. Packages proxied from the upstream are cached, so subsequent installs are served locally.

Each registry is also directly addressable at http://127.0.0.1:7677/npm/~<name>/. For example, you can point only your scope at the hosted registry instead of using the router.

Publish a package

Create a user and publish:

pnpm login --registry http://127.0.0.1:7677/npm/
pnpm publish --registry http://127.0.0.1:7677/npm/

The publish routes through the router to the local hosted registry. Publishing a package that resolves to an upstream, or whose name no registry claims, is rejected.

Where to go next