Private Packagist, Satis and monorepo compared
As soon as an internal Symfony bundle is needed in more than one repository, the question of the right distribution path arises. This article compares Private Packagist, a self hosted Satis and monorepo strategies, including Composer configuration and CI safeguards, so bundles arrive reliably and versioned across multiple projects.
Table of contents
- 1. Why internal bundles need to be distributed at all
- 2. The three common distribution paths at a glance
- 3. Private Packagist in detail
- 4. Satis: your own, self hosted repository
- 5. The middle ground: VCS repositories without a package server
- 6. CI safeguards for shared bundles
- 7. Managing access rights and auth tokens
- 8. When a monorepo pays off instead
- 9. Private Packagist vs. Satis vs. VCS repository
- 10. Summary
- 11. FAQ
1. Why internal bundles need to be distributed at all
An internal Symfony bundle only realizes its full reuse potential once it can be reliably pulled into several independent project repositories, without each project maintaining its own copy of the bundle code. Composer is already the right tool for this, what is missing is simply a place where the package can be found and version managed for every involved project. Without this central place, teams quickly end up back at copy pasting or fragile git submodules.
The difference from public Composer packages: an internal bundle typically should not end up on Packagist.org, because it contains company specific logic, internal API integrations, or simply has no general relevance to the public. Established solutions exist for exactly this case, offering the same Composer experience as a public package while restricting access to authorized teams.
Anyone wanting to share a Symfony bundle across projects should make this decision early, ideally with the very first bundle in the company, because switching the distribution path later means composer.json has to be adjusted in every consuming project. The more projects already reference a bundle, the more expensive such a switch becomes later, since every change has to be coordinated and tested.
An often overlooked benefit of an early, clean distribution strategy: it forces teams to think about versioning from the start, instead of treating bundles as constantly changing, untagged main branches. A bundle without real releases can technically be referenced through a VCS repository, but it prevents consuming projects from deciding for themselves when to adopt a new version.
2. The three common distribution paths at a glance
Three approaches have become established in practice for internal Symfony bundles. Private Packagist is a hosted service that works like Packagist.org but offers access control per team or organization. Satis is a Composer owned tool for generating your own static package repository, which can be hosted on any web server or even inside a simple git repository. The third path, direct VCS repositories in Composer, skips a package server entirely and instead points directly at the bundle's git repository.
All three paths share the fact that they use real version numbers through git tags and that composer update in the target project works normally. The difference lies in operational effort, speed and feature set such as automatic security scans or package usage statistics, which Private Packagist in particular adds on top.
3. Private Packagist in detail
Private Packagist, operated by the Composer team itself, is the lowest maintenance option for companies who want to share a Symfony bundle without running their own infrastructure. After connecting to the git provider, Private Packagist automatically syncs new tags and updates the available versions, without any manual build step needed. Access is controlled through teams and an API token stored in the target project's auth.json.
The Composer configuration entry in the target project is minimal: a composer type repository pointing at the organization's own Private Packagist URL. Composer then automatically queries every configured repository on each update, first Packagist.org for public packages, then the organization's own Private Packagist instance for internal bundles.
A further benefit of Private Packagist that is easily overlooked during initial setup: the service offers automatic security warnings when one of a bundle's dependencies contains a known vulnerability, comparable to Dependabot for GitHub. For companies maintaining several dozen internal bundles, this central overview is often the deciding factor for choosing Private Packagist over a self hosted solution.
{
"repositories": [
{
"type": "composer",
"url": "https://repo.packagist.com/acme-corp/"
}
],
"require": {
"acme/audit-bundle": "^2.3",
"acme/notification-bundle": "^1.0"
}
}
4. Satis: your own, self hosted repository
Satis is the free alternative for teams who do not want an external dependency on a hosted service. It is a static package repository generated from a satis.json configuration and afterwards existing as a plain collection of files on any web server. The main downside compared to Private Packagist: Satis has to be actively rebuilt through a cron job or CI pipeline whenever a new tag appears in one of the referenced bundle repositories, otherwise target projects do not see the new version.
For teams with existing CI/CD infrastructure, this extra build step is usually unproblematic, since it fits in as another pipeline job next to the deployment jobs already in place. The upside: full control over the infrastructure, no ongoing costs for an external service, and the option to run Satis itself behind an internal firewall when bundles contain particularly sensitive logic.
# Regenerate the static Satis repository after a new bundle tag was pushed
php satis.phar build satis.json public/
# Typical cron entry: rebuild every 10 minutes to pick up new tags
*/10 * * * * php /opt/satis/satis.phar build /opt/satis/satis.json /var/www/packages.internal
The generated packages.json and its associated metadata end up in the specified output directory and afterwards only need to be served by a plain web server, a full application server is not required for that.
{
"name": "acme-corp/satis-repository",
"homepage": "https://packages.internal.acme-corp.de",
"repositories": [
{ "type": "vcs", "url": "git@github.com:acme-corp/audit-bundle.git" },
{ "type": "vcs", "url": "git@github.com:acme-corp/notification-bundle.git" }
],
"require-all": true
}
5. The middle ground: VCS repositories without a package server
For a single bundle or a few bundles, even Satis is often not worth it. Composer can reference a git repository directly as a package source, with no generated package repository in between at all. Composer reads composer.json directly from the referenced repository and derives available versions from its git tags. This path is the fastest way to get started, but it scales poorly once ten or more internal bundles need to be distributed, because every target project has to maintain each individual VCS entry in its own composer.json.
A practical middle step: anyone starting with VCS repositories and later switching to Private Packagist or Satis only has to swap the repositories block in every target project, the require lines with package names and version constraints stay unchanged. This low migration cost makes the VCS approach a reasonable starting point as long as the number of internal bundles stays manageable.
{
"repositories": [
{
"type": "vcs",
"url": "git@github.com:acme-corp/audit-bundle.git"
}
],
"require": {
"acme/audit-bundle": "^2.3"
}
}
6. CI safeguards for shared bundles
A Symfony bundle used across multiple projects needs its own CI pipeline inside the bundle repository itself, independent of the pipelines of the consuming projects. This pipeline should include PHPUnit tests with a minimal test kernel, static analysis with PHPStan and a compatibility check against several supported Symfony versions. Without this safeguard, a team only notices a breaking change in the bundle once a consuming project fails after composer update, often far removed from the actual cause.
An additional safety net component is an automated release process that checks on every merge into the main branch whether the composer.json version constraints and the current CHANGELOG entry match up. Tools such as semantic-release can also be adapted for PHP projects, to derive version numbers automatically from commit messages instead of maintaining them manually.
Also important is a documented upgrade path for every major version. An UPGRADE.md in the bundle repository that explicitly lists breaking changes saves consuming teams considerable effort compared to tediously combing through commit history. This discipline pays off especially when a bundle is used by several teams with varying levels of Symfony experience.
# .gitlab-ci.yml — CI pipeline inside the shared bundle repository
stages: [test, release]
test:
stage: test
parallel:
matrix:
- SYMFONY_VERSION: ["6.4.*", "7.0.*", "7.1.*"]
script:
- composer require --no-update "symfony/framework-bundle:${SYMFONY_VERSION}"
- composer update --prefer-stable
- vendor/bin/phpunit
- vendor/bin/phpstan analyse src --level=8
release:
stage: release
only: [tags]
script:
- echo "Tag ${CI_COMMIT_TAG} triggers Private Packagist sync automatically"
7. Managing access rights and auth tokens
Regardless of which distribution path a team chooses, access to private bundle repositories must be governed through authentication. With Private Packagist this happens through an organization token stored in Composer's global or project specific auth.json, never directly in the versioned composer.json. With Satis or direct VCS access, an SSH deploy key or a Composer specific GitHub or GitLab token is usually used instead.
A common security mistake: auth tokens accidentally end up in a Docker image or a CI log, because composer install is run without particular care inside a built image. Multi stage Docker builds, where composer install only runs in the build stage and auth.json never reaches the final image, are the usual safeguard against this. In CI pipelines the token always belongs in a variable marked as secret, never as plain text in the pipeline configuration.
// auth.json — never commit this file, keep it outside version control
{
"http-basic": {
"repo.packagist.com": {
"username": "token",
"password": "acme-corp-private-packagist-token"
}
},
"github-oauth": {
"github.com": "ghp_exampleTokenForDeployAccessOnly"
}
}
8. When a monorepo pays off instead
Not every team benefits from the effort of running its own package repository. If every project using a Symfony bundle already lives in the same repository or is deployed in tight coordination anyway, a monorepo with Composer path repositories can be a simpler alternative. Changes to the bundle are then immediately visible in every application, without a release cycle, which speeds up development but also means a broken commit immediately affects every application.
The choice between a monorepo and a separately distributed bundle mostly depends on how independently the consuming projects should be deployable from each other. Projects with different release cycles and different teams almost always benefit from a real, versioned package repository, because it lets them decide for themselves when to adopt a bundle update. A monorepo can also be organizationally harder to maintain once several teams with different access rights have to work in the same repository.
In practice, many companies choose a hybrid approach: bundles tightly coupled to a single product stay inside that product's monorepo, while truly cross product bundles such as an audit log or a payment integration are distributed through Private Packagist or Satis. This separation avoids unnecessary distribution overhead for code that is only ever needed in one place anyway.
9. Private Packagist vs. Satis vs. VCS repository
The table below summarizes the key differences between the three distribution paths for shared Symfony bundles.
| Criterion | Private Packagist | Satis | Direct VCS repository |
|---|---|---|---|
| Operational effort | None, hosted service | Own build job and hosting | None, uses existing git |
| Cost | Ongoing license fees | Free, hosting only | Free |
| Scaling beyond 10+ bundles | Very good | Good, with CI automation | Poor, every entry manual |
| Extra features | Security scans, statistics | None, plain repository | None |
| Suited for | Companies with many internal packages | Teams with existing CI infrastructure | One or two bundles, fast start |
For most teams wanting to share a Symfony bundle across projects who already operate CI infrastructure, Satis is the pragmatic middle ground between cost and convenience. Private Packagist pays off as soon as the number of internal bundles grows and the maintenance effort of a self hosted Satis exceeds the license price. A realistic decision grid: up to three internal bundles rarely justify more than a direct VCS repository. Between four and roughly twenty bundles, a self hosted Satis usually pays off, because operating costs stay low. From twenty or more bundles, especially across several teams, the benefits of Private Packagist through automatic synchronization and built in security warnings clearly outweigh the ongoing license costs.
Mironsoft
Symfony bundle infrastructure, Composer repositories and CI pipelines
Internal bundles without a central distribution strategy?
We set up Private Packagist or a self hosted Satis for your Symfony bundles, including a CI pipeline with matrix tests against multiple Symfony versions and secure token management.
Repository setup
Private Packagist, Satis or VCS depending on the number and sensitivity of your bundles
CI pipeline design
Matrix tests, PHPStan and automated releases for shared bundles
Security
Auth token management and multi stage Docker builds without leak risk
10. Summary
Sharing a Symfony bundle across multiple projects needs more than just a Composer package, it needs a central, versioned distribution path. Private Packagist is the lowest maintenance option without running your own infrastructure, Satis is the free middle ground with its own build job, and direct VCS repositories are the fastest start for a few bundles. All three paths use real git tags as version numbers and work seamlessly with composer update.
Regardless of the chosen path, a shared bundle needs its own CI pipeline with matrix tests against several Symfony versions, so breaking changes are caught before release instead of only in a consuming project. Clean management of auth tokens outside the versioned composer.json rounds off a secure setup. Anyone who follows these fundamentals can share a Symfony bundle across projects without version chaos or security gaps emerging.
Sharing Symfony Bundles Across Projects — At a glance
Private Packagist
Hosted service with no own operations, automatic synchronization of new tags.
Satis
Free, self hosted repository, needs its own build job via CI or cron.
CI safeguards
Matrix tests against several Symfony versions inside the bundle repository itself.
Security
Auth tokens only in auth.json or secret CI variables, never in the versioned composer.json.