Composer-managed WordPress in one paragraph
WP Packages is the second open source Composer repository for the WordPress.org directory. WPackagist has been doing the same job since 2013. The interesting story is not “which mirror wins” but the fact that in 2026 every WordPress shop running CI/CD, multi-environment deploys, or even a single Bedrock project benefits from treating plugins as Composer dependencies. The dashboard install button does not survive contact with a deploy artifact.
The rest of this guide walks the Roots stack (Bedrock, Sage, Trellis), the trade-offs between WP Packages and WPackagist, the premium plugin problem nobody likes to talk about, and the failure modes that trip up teams switching off the dashboard install workflow.
What Roots actually shipped
In March 2026 the Roots team (Scott Walkinshaw, Ben Word, and contributors) launched WP Packages at wp-packages.org. It mirrors every free plugin and theme from the WordPress.org directory and serves them as a Composer JSON repository.
The launch was not smooth. The project shipped as “WP Composer” on a Tuesday. By Friday Nils Adermann (Composer co-creator, also a contributor to the SemVer spec) had reached out to flag that Composer is a registered trademark held by the Composer project. Roots renamed it to WP Packages over the weekend, kept the existing wp-composer.org URLs answering with a 410 plus deprecation notice, and pushed a Bedrock template update so new projects pointed at the renamed host. This is the kind of incident that would have taken a corporate vendor six weeks of legal review. Open-source projects fix it before the postmortem blog post lands.
The composer.json that replaces your dashboard
{
"repositories": [
{
"type": "composer",
"url": "https://wp-packages.org"
}
],
"require": {
"php": ">=8.2",
"wp-packages/advanced-custom-fields": "^6.3",
"wp-packages/woocommerce": "^9.4",
"wp-packages/wordpress-seo": "^23.0"
}
}
composer install then resolves a tree, writes composer.lock, and downloads ZIPs into web/app/plugins/ (Bedrock layout) or wp-content/plugins/ (vanilla WordPress with composer/installers). On a 25-plugin Bedrock project the warm-cache install runs in 30 to 90 seconds. Cold cache, no Composer cache directory, fresh CI runner: 2 to 4 minutes, mostly download time. That number matters once you start chaining composer install into every PR build.
What self-hosting actually buys you
Self-hosting WP Packages is the one feature WPackagist does not match. Three real scenarios where it pays off:
- Supply-chain audit. Some agency clients require every dependency to be served from infrastructure they control. With WP Packages you fork the project, mirror it onto an internal endpoint, and your
composer.jsonno longer talks to a third-party host during deploy. - Air-gapped CI runners. Jenkins or GitLab Runners inside a customer VPN cannot reach wpackagist.org or wp-packages.org. With self-hosted WP Packages plus a local Composer cache, the deploy pipeline is offline-clean.
- Custom filtering. A real example: an agency blocked plugins last updated more than 18 months ago from being installable. They forked WP Packages, added the filter at index time, and
composer requireon a stale plugin now fails with a clear error instead of silently installing abandoned code.
For most teams, WPackagist is fine. The above are the cases where it stops being fine.
WPackagist: still the default for a reason
WPackagist is run by Outlandish, a UK cooperative. It has been mirroring the WordPress.org directory since 2013 with very few outages. The project’s package naming (wpackagist-plugin/woocommerce, wpackagist-theme/twentytwentyfour) became the de-facto convention; nearly every Bedrock tutorial written before March 2026 assumes WPackagist.
Side-by-side
| Property | WPackagist | WP Packages |
|---|---|---|
| Live since | 2013 | March 2026 |
| Maintained by | Outlandish | Roots |
| Open source repository code | Closed | Yes (MIT) |
| Self-hostable | No | Yes |
| Vendor prefix | wpackagist-plugin/, wpackagist-theme/ | wp-packages/ |
| Sync method | Cron polls WordPress.org SVN | Cron polls WordPress.org API |
| Bedrock skeleton default | Yes (today) | No (manual swap) |
| Operational track record | 12+ years | A few weeks |
When to switch, when not to
Switch to WP Packages if you need self-hosting, you have already standardised on the Roots stack and prefer single-vendor support, or you have hit a WPackagist sync lag that broke a build and want a second mirror as backup. Stay on WPackagist if your CI is green, your composer.lock is reproducible, and “let us migrate the vendor prefix on 200 client sites” is not on anyone’s roadmap.
The two are not mutually exclusive. Nothing stops you from declaring both repositories and pinning specific plugins to specific mirrors. That is what most agencies will end up doing in practice.
Bedrock, Sage, Trellis: where Composer actually pays off
The Roots stack is the reason Composer-on-WordPress feels native instead of bolted-on.
Bedrock: the project layout
Bedrock is a project skeleton, not a framework. What it actually does:
- Splits WordPress core into
web/wp/(Composer-managed) sowp-config.phpis a thin loader instead of the canonical config file. - Moves all environment configuration into
.env(read byvlucas/phpdotenv) andconfig/environments/{development,staging,production}.php. TheWP_ENVconstant drives which environment file loads. - Treats
web/app/plugins/,web/app/themes/, andweb/app/mu-plugins/as Composer install targets viacomposer/installers. - Ships a
mu-plugins/register-theme-directory.phpautoloader socomposer requireon a theme actually registers it with WordPress.
.env is gitignored. Production secrets live in your secret manager, not in wp-config.php. This is the contract that makes WP_DEBUG, ACF Pro license keys, and database credentials per-environment safe instead of “did anyone push the wrong wp-config.php to staging again?”.
composer create-project roots/bedrock my-project
cd my-project
cp .env.example .env
# edit .env with local DB credentials and WP_ENV=development
composer install
Sage 10 to Sage 11: the migration tax
Sage is the Roots theme. Sage 11 (released 2024) brought Acorn, a Laravel-style container running inside WordPress, plus Blade templates instead of plain PHP. Sage 10 to Sage 11 migrations have specific failure modes worth knowing before you sign up:
- Acorn breaking changes. Hooked actions registered through Acorn’s service provider mechanism fire later than the same hooks registered in
functions.php. Code that assumedinitpriority 10 may need to move to priority 20 or to a different hook entirely. - Blade vs PHP templates.
single.blade.phpinstead ofsingle.php. Oldget_template_partcalls still work but bypass the Blade pipeline and lose the section/yield mechanics. - Asset pipeline. Sage 10 used Bud (Webpack 5 wrapper); Sage 11 moved to Vite. Bud config files are not portable. Your
tailwind.config.jsis, but watch for PostCSS plugin order differences.
The migration is not push-button. Budget two days for a small theme, a week for anything with custom Gutenberg blocks.
Trellis: the option you probably do not need
Trellis is Ansible plus Vagrant for local development and remote provisioning. It builds Ubuntu LTS servers with Nginx, MariaDB, PHP-FPM, Redis, and a deploy pipeline. Real talk: Trellis is excellent for shops that own their infrastructure (think 50 to 200 sites on dedicated servers) and overkill for everyone else. If you are deploying to managed WordPress hosting (Kinsta, WP Engine, Pressable), Trellis is not the right tool. If you are running your own DigitalOcean fleet, it is.
Failure modes nobody mentions in the introduction
These are the four ways composer install ruins someone’s afternoon.
PHP version drift between local and CI
composer require resolves against the PHP version it sees today. A plugin marked "php": ">=8.2" installs fine on your laptop running PHP 8.3. A staging environment frozen on PHP 8.1 will fail composer install with an unhelpful platform-requirement error. Pin config.platform.php in composer.json to the lowest version any environment runs:
{
"config": {
"platform": {
"php": "8.2.20"
}
}
}
This single line saves more deploys than any CI optimization.
mu-plugins/register.php loaded against the wrong WordPress version
Bedrock’s mu-plugins/ autoloader runs at PHP boot time, before WordPress core is initialized. If you composer update WordPress core to a version that changed WP_PLUGIN_DIR semantics (this happened in WP 6.5), the autoloader can register theme directories that no longer exist. Symptom: white screen on production, fine on staging. Fix: lock roots/wordpress to a known-good minor and bump it deliberately.
ACF Pro license activation in deploy environments
ACF Pro authenticates its license against advancedcustomfields.com. Deploy artifact builds (think Bitbucket Pipelines, GitHub Actions) run as a fresh container with no DOM, no admin session, and an IP that the ACF license server has never seen. The activation flow that works in wp-admin does not work in a CI runner. Fixes that actually work in 2026:
- Use the Roots Premium proxy if you are on a Roots organization plan (it brokers ACF Pro and other premium licenses through Composer).
- Set
ACF_PRO_LICENSEas an environment variable read by ACF’s PHP API and skip the dashboard activation flow entirely. - Use Composer’s HTTP basic auth (
auth.json) to authenticate against ACF’s official Composer endpoint atconnect.advancedcustomfields.com.
The first two are the only ones that survive a server migration without manual intervention.
Composer 2.7 vs Composer 2.6 lockfile incompatibility
Composer 2.7 changed how composer.lock records platform overrides. A composer.lock written by 2.7 fails to install on 2.6 with a misleading “the lock file is not up to date” error. Standardise the Composer version in CI (composer self-update 2.7.7) and document it in your README. Yes, this is annoying. Yes, it has bitten enough teams that Composer 2.8 will warn about it explicitly.
Premium plugins: the part the docs gloss over
Half the plugins on a real client site are not in the WordPress.org directory. ACF Pro, Gravity Forms, WP Migrate, GP Premium, FacetWP, WP All Import Pro. Composer support varies wildly:
- ACF Pro: official Composer endpoint at
connect.advancedcustomfields.com. Works well, requiresauth.jsonwith your license key as the password. - Gravity Forms: official Composer support since 2023. License key in
auth.json, package namegravityforms/gravityforms. - GP Premium (Generate Press): no official Composer endpoint. SatisPress or Roots Premium proxy.
- WP Migrate Pro (Delicious Brains, now WP Engine): ZIP download with a license-keyed URL. Use a custom Composer repository declaring the package as a
packagetype with the URL inline, or proxy through SatisPress. - WP All Import Pro: no Composer endpoint. ZIPs hosted on a license-locked URL. SatisPress or commit the ZIP to a private repository.
Three patterns cover all of these:
- Roots Premium (roots.io/premium). A managed Composer proxy that handles licenses for ACF Pro, Gravity Forms, and others. Pricing is individual; for agencies running 50+ sites the math usually works.
- SatisPress. A WordPress plugin that turns your own WordPress install into a Composer repository. Install premium plugins normally, expose them via
https://your-private-host/satispress/, lock them down with HTTP basic auth. - Private Packagist by packagist.com. Hosted Composer repository with private package support. Costs more than SatisPress, requires zero infrastructure.
Whichever you pick, the rule is: license keys never go in composer.json or auth.json checked into git. They live in environment variables read at install time, or in CI secrets injected into auth.json during the build step.
CI integration that survives reality
A working deploy pipeline:
# .github/workflows/deploy.yml (excerpt)
- name: Set up PHP 8.2
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer:2.7
- name: Cache Composer
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: composer-${{ hashFiles('composer.lock') }}
- name: Install
run: composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction
env:
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH_JSON }}
- name: Deploy artifact
run: rsync -avz --delete --exclude='.git' --exclude='node_modules' ./ deploy@host:/var/www/html/
Three rules that will save you debugging time:
- Never run
composer updatein CI. Update locally, commit the newcomposer.lock, push. CI runscomposer installagainst the exact lockfile. - Cache
~/.composer/cachekeyed oncomposer.lockhash. Cold installs go from 4 minutes to 30 seconds. - Inject
auth.jsonfrom a secret.COMPOSER_AUTHenv variable is read by Composer natively. Never commit licensed credentials.
What changed in WordPress culture in 2026
Composer-on-WordPress used to be a Roots-shop thing. In 2026 it is the default for any agency or in-house team that ships to more than one environment. Three signals from the wider ecosystem:
- WordPress.org plugin submissions cleared 500 per week in early 2026, up from 100 to 150 weekly through 2022 to 2024 (source: weekly Plugin Review Team reports). Most of the new volume is AI-assisted code. The Plugins Team is hiring volunteer reviewers; the directory’s quality bar is under strain.
- Both Kinsta and WP Engine added native Composer build support to their Git push deployment in late 2025. The “managed WordPress host that ignores composer.json” category is shrinking.
- WP-CLI 2.10 added
wp composersubcommands that wrapcomposer install,composer update, and lockfile validation. Composer is becoming part of WP-CLI’s official surface area.
The lesson for plugin-as-dependency management: the tooling is maturing faster than the WordPress.org directory’s review capacity. A self-hostable mirror like WP Packages is more useful in this context than it would have been five years ago, when the directory was a closed garden everyone trusted by default.
Last updated: 2026-04-01
Need help migrating a site to Bedrock or setting up a Composer-driven deploy pipeline? See our WordPress development services.
