a private Composer registry without credential chaos
Once a team maintains more than a handful of internal PHP packages, distributing deploy keys and personal GitHub tokens in every single composer.json quickly becomes a security risk. Private Packagist bundles private Composer packages in its own registry with centralized permission management, without developers having to maintain their own credentials.
Table of Contents
- 1. Why a team needs a private Composer registry
- 2. Setting up Private Packagist: organization and first packages
- 3. Connecting sources: GitHub, GitLab and your own Git repositories
- 4. Composer auth: auth.json instead of individual tokens
- 5. Controlling access rights per team and per package
- 6. GitHub mirroring and resilience
- 7. Integrating into CI pipelines without leaking tokens
- 8. Alternatives: Satis, Toran Proxy and self built solutions
- 9. Private Packagist compared to self hosted solutions
- 10. Summary
- 11. FAQ
1. Why a team needs a private Composer registry
Private Packagist is the paid service operated by the Composer company Tidelift for hosting private PHP packages, built on the same technology as the public Packagist. Without such a service, a team usually attaches internal packages through the vcs repository type, pointing directly at the private Git repository with a personal access token per developer stored in the local auth.json. That works for a single package but quickly becomes unmanageable with ten or more internal packages.
The real problem is not the technology, it is permission management. When a developer leaves the team, every issued token for every single repository has to be revoked. New team members need access to potentially dozens of repositories before the first composer install even works. Private Packagist solves this by placing a single access layer in front of all private packages: a team member gets one single token for the organization, not one per repository.
On top of that, Private Packagist uses the same metadata resolution as the public Packagist: version information is extracted from Git tags, composer.json changes are detected automatically, and security warnings from the Roave Security Advisories database also show up for private packages whenever a known vulnerability appears in a used dependency.
2. Setting up Private Packagist: organization and first packages
Setup begins with an organization at packagist.com, the commercial counterpart to packagist.org. Inside the organization, an administrator creates the first package source, usually by connecting a GitHub or GitLab account through OAuth. Private Packagist then automatically scans all repositories the connected account has access to and suggests which ones should be imported as Composer packages.
For every imported package, Private Packagist automatically detects the package name from the composer.json in the repository and immediately starts resolving tags and branches as versions. An important difference from the public Packagist: private packages never show up in the public search and are only visible to members of the respective organization, even if the package name happens to collide with a public package.
{
"name": "mironsoft/internal-billing",
"type": "library",
"require": {
"php": "^8.4"
},
"repositories": [
{
"type": "composer",
"url": "https://repo.packagist.com/mironsoft-gmbh/"
}
],
"require-map": {
"mironsoft/internal-billing": "^4.2"
}
}
3. Connecting sources: GitHub, GitLab and your own Git repositories
Besides GitHub and GitLab, Private Packagist also supports Bitbucket as well as any SSH reachable Git repository through a manually stored URL. For GitHub and GitLab, the connection runs through an official app integration that automatically registers webhooks. Every push to a branch with a valid composer.json immediately triggers a new metadata update in the registry, without anyone having to click a manual sync button.
For repositories outside the supported platforms, for example a self hosted GitLab CE or an internal Gitea, you store an SSH deploy key that Private Packagist uses for read access. In that case the automatic webhook is missing, instead the service polls the repository at regular intervals for new tags, which in practice means a delay of a few minutes before a new version becomes visible.
# Generate a dedicated deploy key for Private Packagist (read only)
ssh-keygen -t ed25519 -C "private-packagist-readonly" -f ./packagist_deploy_key -N ""
# Add the public key as a read-only deploy key on the self-hosted Git server
cat ./packagist_deploy_key.pub
# Paste the output into: Gitea/GitLab CE -> Repository -> Deploy Keys -> Read only
# Paste the private key into Private Packagist's package source configuration
cat ./packagist_deploy_key
4. Composer auth: auth.json instead of individual tokens
The decisive advantage in everyday development shows up in auth.json. Instead of maintaining a separate entry under http-basic or github-oauth for every private Git repository, a single entry for the Private Packagist organization URL is enough. Composer then authenticates against exactly one source and receives from there every package for which the stored token has read permission.
The token itself is no longer generated per repository but per organization in Private Packagist, and can be granularly restricted to individual teams. When a developer leaves the company, revoking this one token is enough to immediately end all access to every private package, without having to navigate through dozens of Git repositories.
# Configure Composer to authenticate against Private Packagist
composer config --global --auth http-basic.repo.packagist.com token "$PACKAGIST_TOKEN"
# Verify the auth.json entry was written correctly
cat "$(composer config --global home)/auth.json"
# Install a private package once auth is configured
composer require mironsoft/internal-billing:^4.2
5. Controlling access rights per team and per package
Private Packagist organizes access rights across two layers: organizations and teams nested within them. A Backend team can, for example, get read access to all core packages, while a Mobile team stays restricted to only the API packages relevant to mobile clients. This separation prevents every team member from automatically getting access to every internal package of the company, even if they do not need it for their own work at all.
In addition, Private Packagist distinguishes between read and write permissions at the package level. A CI system usually only needs read access to run composer install, while only a few people should be granted write permission to create new package sources. This fine grained separation significantly reduces the attack surface compared to a single deploy key valid for everything.
In practice this structure maps directly onto the management interface: an organization contains several teams, each team is assigned a list of package groups, and each team member is assigned to exactly one or more teams. A new developer on the Mobile team therefore only sees the packages released to them on their first composer install, without an administrator having to manually walk through individual repository permissions.
{
"organization": "mironsoft-gmbh",
"teams": [
{
"name": "backend-core",
"permission": "read",
"packages": [
"mironsoft/internal-billing",
"mironsoft/core",
"mironsoft/http-client"
]
},
{
"name": "mobile-clients",
"permission": "read",
"packages": [
"mironsoft/mobile-api-contracts"
]
},
{
"name": "release-managers",
"permission": "write",
"packages": ["*"]
}
]
}
6. GitHub mirroring and resilience
An often overlooked feature of Private Packagist is automatic dist mirroring. The service automatically downloads an archive of the corresponding commit for every detected tag and stores it redundantly in its own storage. If the original GitHub or GitLab repository temporarily goes down, for example due to an outage at the hosting provider, composer install still works because Private Packagist serves the archived dist package directly, without needing renewed access to the original source.
This mirroring is especially relevant for CI pipelines that have no time during a deployment for a failed composer install caused by an external outage. A GitHub outage then only affects new pushes, not the installation of already tagged versions, which noticeably increases the resilience of the entire build pipeline.
7. Integrating into CI pipelines without leaking tokens
For CI systems such as GitLab CI, GitHub Actions or Jenkins, the Private Packagist token is stored as a protected CI secret, never in plain text in the repository. At runtime the token is written into a COMPOSER_AUTH environment variable in JSON format, which Composer automatically evaluates instead of the local auth.json. This approach avoids credentials ever landing on the CI runner's disk as a file at all.
A separate, read only token per CI pipeline, kept apart from the personal developer token, is the recommended practice. If a CI token gets compromised, for example through a misconfigured log output, it can be revoked in isolation, without developers having to set up their own local credentials again.
# .gitlab-ci.yml — inject Private Packagist token as JSON auth, never as a file in the repo
install:
stage: build
variables:
COMPOSER_AUTH: '{"http-basic":{"repo.packagist.com":{"username":"token","password":"$PACKAGIST_CI_TOKEN"}}}'
script:
- composer install --no-dev --optimize-autoloader --no-progress
8. Alternatives: Satis, Toran Proxy and self built solutions
Anyone who does not want to use a third party service can build a self hosted, static Composer repository with composer/satis. Satis generates a packages.json file once or through a cron job from configured source repositories, which is then served through any web server. The advantage lies in full control over the infrastructure, the disadvantage in missing automatic mirroring, missing granular permission management and the need to operate and monitor the cron job yourself.
As a middle ground, private Composer repository server software such as Toran Proxy or a self operated Satis behind an Nginx basic auth is an option. For small teams with few internal packages, this is often sufficient, but for growing organizations with several teams and frequent staff turnover, the maintenance effort of a self hosted solution quickly becomes disproportionately high compared to Private Packagist.
9. Private Packagist compared to self hosted solutions
The choice between Private Packagist and a self hosted alternative such as Satis depends heavily on team size, maintenance budget and security requirements. The table below compares the most important differences.
| Criterion | Satis (self hosted) | Private Packagist | Practical relevance |
|---|---|---|---|
| Access rights per team | Manual through Nginx auth | Granular through UI, teams and organizations | High with multiple teams |
| Automatic update on push | Cron job required | Webhook, instant update | Important for fast releases |
| Dist mirroring on outage | Not included | Built in automatically | Increases CI resilience |
| Operating cost | Server hosting only | Monthly subscription | Relevant for very small teams |
| Maintenance effort | Own hosting, own updates | Fully managed service | Saves operational time for the team |
For teams under five developers with two to three internal packages, a self hosted Satis is often entirely sufficient and significantly cheaper. As soon as multiple teams with different access requirements, frequent staff turnover or strict compliance requirements come into play, the management advantage of Private Packagist clearly outweighs the extra cost in most cases.
Mironsoft
PHP architecture, package strategy and Composer tooling
Private Composer packages without credential chaos in your team?
We set up Private Packagist or a self hosted alternative for your team, including a permission concept, CI integration and migration of your existing private packages.
Needs Assessment
Assessing whether Private Packagist or a self hosted solution is the better fit
Setup
Organization, teams, access rights and source connections cleanly configured
CI Integration
Secure token management without leaked credentials in pipelines
10. Summary
Private Packagist for a team solves the fundamental problem of scattered credentials for private Composer packages: instead of personal tokens per Git repository, a single registry manages all access rights centrally, granularly split across teams and packages. Connecting GitHub, GitLab and self hosted Git repositories runs through automatic webhooks or SSH deploy keys, while Composer auth gets by with a single auth.json configuration instead of dozens of individual repository entries.
For CI pipelines, a separate, read only token ensures a clean separation between personal developer access and the automated build process. Automatic dist mirroring increases resilience against outages at the actual Git host. For small teams, a self hosted Satis remains a valid, cheaper alternative, but as soon as multiple teams and frequent staff turnover enter the picture, the management advantage of Private Packagist clearly wins out.
Private Packagist in a Team — The Essentials at a Glance
Centralized Permission Management
A single token per organization instead of personal credentials for every individual private Git repository.
Teams and Access Levels
Granular separation by team and package, instead of automatic full access for every team member.
Dist Mirroring
Automatically archived package versions secure composer install even if the original Git host goes down.
CI Integration
COMPOSER_AUTH as a CI secret instead of credential files, with a separate, read only token.