Every growing Laravel team eventually extracts a package: the shared billing client, the internal logging helper, the theme. Then comes the assumption that serving it needs infrastructure - Satis, Private Packagist, a registry. For most teams it does not: Composer reads plain Git repositories directly, and if you already self-host your Git, the registry is running.
The VCS repository#
In the consuming app, tell Composer where the package lives:
"repositories": [
{ "type": "vcs", "url": "[email protected]:shared/billing-client.git" }
],
"require": {
"yourcompany/billing-client": "^1.2"
}
The package repo itself just needs a composer.json with its name and autoload config - no extra hosting. On install, Composer clones the repo over SSH and checks out the requested version. That is the whole registry.
Versions come from tags#
Composer reads versions from Git tags, which makes disciplined tagging the whole game: tagging releases properly covers annotated semver tags, and the contract here is simple - tag 1.2.0 in the package repo and ^1.2 resolves to it. No tag, no release; a branch name works for dev versions (dev-main) but treat that as a development convenience, never a production dependency.
Access is the only real setup#
CI and every developer need read access to the package repo. This is where teams reach for a shared key in a wiki - do not. The patterns in deploy keys and machine users apply exactly: a read-only deploy key per consumer, or a machine user with access to the package repos. Developers authenticate with their own SSH keys, which they already have.
When you outgrow it#
The direct-VCS approach slows down at scale: Composer clones every package repo on every install, and metadata lookups multiply. The honest thresholds are dozens of packages or very slow CI installs, and the upgrade path is Satis (static, self-hosted, stays on-brand) generating metadata over the same repos. Until then, VCS entries plus tags cover the need with zero new infrastructure.
Your Git server already hosts the code. Let it host the packages too.
Share this post
Keep reading
Backing Up Your Git Server So It Actually Restores
Every clone is a partial backup - but hooks, config and server state live outside the repo. Mirror, bundle, copy off-site, and rehearse the restore.
Monitoring Your Self-Hosted Git Server: The Four Things That Matter
A Git server fails quietly: disks fill, backups rot, SSH dies at 2am. The four checks that catch every realistic failure, and how to keep them boring.
Moving Your Repositories to a New Git Server Without Losing Anything
Git makes migration nearly lossless - every clone carries the full history. The mirror-push recipe, what does not travel with the repo, and the order to do things in.