skip to content

Packagist.org serves only public packages; how should a team distribute private PHP packages to its own projects instead?

level: seniorimportance: should knowfreq 32%

answer

  1. public index, public metadata
  2. vcs entry per package
  3. Satis builds a static registry
  4. hosted private registry
  5. auth.json, never composer.json

basics

~20 s

Keep private packages off Packagist.org and serve them from a private repository: individual vcs entries for a few packages, a static registry built with Satis, or a hosted private registry. Credentials go in auth.json or environment variables.

solid answer

~40 s

Packagist.org indexes publicly reachable repositories and serves its metadata to anyone, so private code does not belong there. Teams have three options. **Direct vcs repositories**: each project lists each private repository; simple, but every project repeats the list and Composer scans each repository's tags and branches on every resolution. **A static registry built with Satis**: an open-source generator that reads a `satis.json` list of repositories and writes a `packages.json` composer repository, optionally with dist archives, that the team hosts behind authentication. **A hosted private registry**: the same protocol with access control and mirroring managed for you. Either registry becomes one entry in each project instead of many. Credentials live in `auth.json` (`http-basic`, `github-oauth`, `gitlab-token`, `bearer`) or the `COMPOSER_AUTH` environment variable, never in a committed `composer.json`.

code

json · 9 lines
json
{
    "name": "acme/internal-packages",
    "homepage": "https://packages.acme.internal",
    "repositories": [
        { "type": "vcs", "url": "https://git.acme.internal/php/date-phrases" },
        { "type": "vcs", "url": "https://git.acme.internal/php/billing-sdk" }
    ],
    "require-all": true
}

go deeper

for a junior

Know that Packagist.org is public and that private packages come from a private repository configured in the project.

for a middle

Compare vcs entries, a Satis-built static registry and a hosted registry, and know where Composer reads credentials from.

for a senior

Choose the distribution model for a team, keep credentials in auth.json or COMPOSER_AUTH, and make sure private packages are covered by advisories too.

for a principal

Own the registry decision across the organisation: who runs it, who may publish, availability of installs, and cost against the time lost to scattered vcs entries.

## Why not Packagist.org **Packagist.org** is a public index. It crawls repositories it can reach and publishes their metadata (names, versions, requirements, download locations) to every Composer client in the world. Private code has no place there. Even when the code host requires authentication, the metadata would still announce the package's existence and structure. Private packages need a **private repository**: a source that only your projects are configured to read and only authenticated users can fetch from. ## The three ways teams do it | Option | What you run | Good for | Costs | |---|---|---|---| | `vcs` repositories | nothing extra | two or three private packages | every project repeats the list; Composer scans each repository on resolution; credentials per code host | | **Satis** | a static `composer` repository you build and host | many packages, full control | you run the build job, the hosting and the authentication | | hosted private registry | a service speaking the `composer` repository protocol | teams that want access control and mirroring managed | a subscription and a third party in the install path | ### Direct vcs repositories Each project adds each private repository as a `vcs` entry. It works immediately, but it scales badly. Repositories declared by a dependency are **not** loaded for the root project, so a private package that depends on another private package forces every consuming project to list both. Resolution also slows down, because Composer reads tags and branches from each repository itself. ### Satis **Satis** is Composer's open-source static repository generator. You give it a `satis.json` naming the repositories to include (often `vcs` entries), and either `"require-all": true` or an explicit list of packages. It writes a `packages.json` and the per-package metadata files, and can also build dist archives. You host the output on any web server behind authentication, re-run the build when packages change (typically from the same hooks you would point at Packagist), and each project needs a single `composer` repository entry. ### A hosted private registry Commercial registries implement the same `composer` repository protocol with user management, per-package permissions and mirroring of dist files. The mirroring means installs keep working when a code host is down. The trade-off is operational: someone else runs it, and it sits on your install path. ## Credentials Composer reads credentials from **`auth.json`** (in the project directory or the global Composer home) or from the **`COMPOSER_AUTH`** environment variable, which holds the same JSON. The supported forms include: - `http-basic`: a username and password (or token) per host; - `bearer`: a bearer token per host; - `github-oauth`, `gitlab-token` and similar: tokens for code-host APIs. Rules that keep this safe: 1. **Never put credentials in a repository URL in `composer.json`.** That file is committed and shared. 2. **Keep a project-level `auth.json` out of version control**, or better, inject `COMPOSER_AUTH` from the CI secret store. 3. **Give CI a read-only token** scoped to the packages it needs. ## Advisory data for private packages Packagist.org publishes security advisories for public packages. A private `composer` repository can publish advisories too: its `packages.json` may advertise a `security-advisories` section with an `api-url` or per-package metadata, and Composer then blocks and audits private packages as well. Without that, advisories only exist for what Packagist knows about. ## Keeping a private registry current A private registry has the same freshness problem as Packagist. A static registry built with Satis only knows about tags that existed at its last build, so teams trigger the build from a hook on each private repository or on a short schedule. A hosted registry typically updates from hooks as well. When a new internal release "does not exist", the first check is the same as on Packagist: does the registry's `packages.json` list it yet? ## Choosing - A handful of private packages, one or two projects: `vcs` entries are fine. - A growing internal library set: a registry, built with Satis or hosted, restricted with `only` to your vendor prefix and listed above Packagist. - Many teams: a hosted or self-run registry with per-team access control, plus an audit trail of who can publish.

  • Why does listing private repositories inside a private package's own composer.json not work?
    Composer loads repositories from the root package only. A dependency's `repositories` section is ignored, so the root project must list every private source itself. A single private registry that serves all internal packages removes that repetition.
  • How do private packages get security advisories?
    Packagist.org only covers public packages. A private `composer` repository can advertise a `security-advisories` section in its `packages.json`, with an `api-url` or per-package metadata, and Composer then uses it for blocking and `composer audit`. Otherwise, internal packages have no advisory data at all.

saying these in an interview costs you the question

  • Submitting a private repository to Packagist.org keeps its metadata private.
  • Credentials can go in the repository URL in composer.json.
  • A private package's own repositories list is loaded for consumers.
  • Satis is a hosted service you subscribe to.
  • vcs entries scale well to dozens of private packages.