When a digital agency manages 5 to 10 WordPress websites, managing custom code is relatively straightforward: developers push commits directly to independent GitHub repositories, and plugins are updated via WP Admin or WP-CLI.
When that roster scales to 40, 80, or 150+ client sites, an agency inevitably crashes into the multi-tenant codebase dilemma:
- The Copy-Paste Nightmare: Your agency builds a proprietary custom block library, an enterprise security mu-plugin, or a custom checkout analytics integration. When you fix a bug or add a feature, an engineer must manually copy-paste the patch across 60 separate repositories, create 60 pull requests, and test 60 deploys.
- The Monorepo Fragility: Frustrated by fragmentation, some agencies attempt a single monolithic Git repository containing all client sites. Within months, the monorepo swells to gigabytes, client credentials bleed across branches, and deploying a fix for Client A risks taking down Client B.
- Rampant Configuration Drift: Without deterministic dependency tracking, no two client sites share the same plugin versions, PHP standards, or directory structures. Maintenance becomes an unbillable game of whack-a-mole.
Modern software engineering solved this decades ago through modular dependency management.
In enterprise WordPress engineering, combining Composer (for versioned, reproducible dependency resolution) with Git submodules (for shared internal codebases) provides the ultimate architecture: isolating client configurations while maintaining a single, upstream source of truth.
This technical guide breaks down the multi-tenant GitOps blueprint top-tier agencies use to manage 100+ client sites, details production Composer and submodule configurations, and explains how to integrate automated visual QA and rollback pipelines with SiteOps.
011. Deconstructing the Architecture: Composer vs. Git Submodules
To build a scalable agency infrastructure, engineering leadership must understand the exact operational boundary between Composer and Git submodules:
+-------------------------------------------------------------------------------+
| COMPOSER VS. GIT SUBMODULES BOUNDARY |
+-------------------------------------------------------------------------------+
| COMPOSER (DEPENDENCY RESOLUTION) | Semantic versioning (^2.1.0) |
| | Third-party plugins, WordPress Core, themes|
| | Frozen, reproducible builds via composer.lock|
| |
| GIT SUBMODULES (CODE SHARING) | Pinned to exact Git commit SHA-1 |
| | Proprietary agency frameworks & mu-plugins |
| | Bi-directional active feature development |
+-------------------------------------------------------------------------------+
When to Use Composer:
Composer is the definitive tool for third-party and vendor code:
- WordPress Core & Public Plugins: Pinned through WordPress Packagist (
wpackagist-plugin/*). - Commercial Premium Plugins: Tracked via private Composer repositories (Satis, Private Packagist) for Advanced Custom Fields Pro, Gravity Forms, and WP Migrate.
- Deterministic Deployments: Committing
composer.lockguarantees that staging and production execute bit-for-bit identical code versions.
When to Use Git Submodules:
Git submodules excel where Composer can feel cumbersome for fast-moving agency development:
- Shared Agency Frameworks: A custom parent theme, reusable Gutenberg block suite, or internal WebOps mu-plugin developed actively across multiple client builds.
- Bi-Directional Commits: Developers working inside a client project can enhance an agency utility component, commit the change to the submodule’s origin branch, and immediately push it back to the central repository.
Learn how consistent environments prevent release failures in our guide on Eliminating Staging Environment Drift Using Automated CI/CD.
022. Directory Hierarchy Blueprint: The Modern Twelve-Factor WordPress Setup
To prevent WordPress core and third-party vendor code from cluttering your client repositories, modern agencies adopt the Bedrock directory standard (popularized by Roots):
client-acme-store/
βββ .env.example (Environment variable template)
βββ composer.json (Client-specific dependency manifest)
βββ composer.lock (Locked dependency snapshot)
βββ config/ (Application configuration)
β βββ application.php (Environment-agnostic WP config)
β βββ environments/ (Development, staging, production overrides)
βββ vendor/ (Composer dependencies - Git ignored)
βββ web/ (Public document web root)
βββ index.php (WordPress bootstrapper)
βββ wp/ (WordPress Core - managed by Composer)
βββ app/ (Custom wp-content replacement)
β βββ mu-plugins/ (Must-Use plugins)
β β βββ 00-agency-webops.php (Autoloader bootstrapper)
β β βββ agency-core/ (GIT SUBMODULE: Shared Agency WebOps Suite)
β βββ plugins/ (Third-party plugins - Composer managed)
β βββ themes/
β β βββ agency-parent/ (GIT SUBMODULE: Shared Parent Theme)
β β βββ client-child-theme/ (Client-specific custom child theme)
β βββ uploads/ (Persistent media - symlinked / ignored)
The Architectural Advantages:
- WordPress Core is Disposable: WordPress Core lives in
/web/wp/and is never committed to Git. Upgrading WordPress across 50 sites is as simple as updatingcomposer.jsonand runningcomposer update roots/wordpress. - Decoupled Web Root: Non-public configuration files, secrets (
.env), and build scripts reside outside the publicweb/document root, eliminating direct file access exploits.
033. Production composer.json Manifest for Client Repositories
Below is an enterprise-grade composer.json file configuring WordPress Core, premium plugin repositories, and directory mapping:
{
"name": "agency/client-acme-store",
"description": "Production enterprise WordPress architecture for Acme Store",
"license": "proprietary",
"type": "project",
"authors": [
{
"name": "Agency Engineering Team",
"email": "devops@agency.com"
}
],
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
},
{
"type": "composer",
"url": "https://composer.deliciousbrains.com"
},
{
"type": "package",
"package": {
"name": "advanced-custom-fields/advanced-custom-fields-pro",
"version": "6.3.8",
"type": "wordpress-plugin",
"dist": {
"type": "zip",
"url": "https://connect.advancedcustomfields.com/index.php?a=download&p=pro&k={%ACF_PRO_KEY}&t=6.3.8"
}
}
}
],
"require": {
"php": ">=8.2",
"roots/wordpress": "6.7.1",
"roots/wp-config": "1.0.0",
"roots/wp-password-bcrypt": "1.1.0",
"vlucas/phpdotenv": "^5.6",
"wpackagist-plugin/woocommerce": "^9.4",
"wpackagist-plugin/redis-cache": "^2.5",
"advanced-custom-fields/advanced-custom-fields-pro": "^6.3"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"allow-plugins": {
"composer/installers": true,
"roots/wordpress-core-installer": true
}
},
"extra": {
"installer-paths": {
"web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
"web/app/plugins/{$name}/": ["type:wordpress-plugin"],
"web/app/themes/{$name}/": ["type:wordpress-theme"]
},
"wordpress-install-dir": "web/wp"
}
}
044. Taming Git Submodules: Eliminating Common Developer Pitfalls
While Git submodules are extraordinarily powerful for multi-tenant code sharing, inexperienced developers frequently encounter three operational headaches:
+-------------------------------------------------------------------------------+
| THE 3 GIT SUBMODULE HAZARDS & FIXES |
+-------------------------------------------------------------------------------+
| 1. DETACHED HEAD STATE | Git checks out commit SHA, not active branch |
| | Fix: Configure submodule.recurse & branch tracking|
| |
| 2. BLANK SUBMODULE DIRS | Junior dev clones repo; submodule folders empty |
| | Fix: git clone --recurse-submodules |
| |
| 3. OUT-OF-SYNC CI BUILDS | CI runner lacks deploy keys to private submodule |
| | Fix: Deploy keys / Personal Access Token in CI/CD |
+-------------------------------------------------------------------------------+
1. Adding a Shared Agency Submodule
To attach your agencyβs proprietary shared WebOps suite into a client project:
# Add shared agency core as a submodule inside the mu-plugins directory
git submodule add -b main git@github.com:agency/agency-core.git web/app/mu-plugins/agency-core
git commit -m "feat(webops): integrate shared agency-core submodule"
2. The Universal Clone & Pull Commands
Ensure your agency engineering runbook mandates these commands:
# Clone a client repository with all shared submodules automatically populated
git clone --recurse-submodules git@github.com:agency/client-acme-store.git
# Pull upstream client changes and sync all submodule commits in one step
git pull --recurse-submodules
3. Pushing Submodule Updates Across the Fleet
When a developer updates agency-core inside Client A and wants to roll the feature out to 50 other client sites, execute an automated batch update via bash:
#!/usr/bin/env bash
# ==============================================================================
# Multi-Tenant Submodule Fleet Synchronization Script
# ==============================================================================
set -euo pipefail
CLIENT_REPOS_DIR="/home/devops/clients"
echo "==> Synchronizing agency-core submodule across all client repositories..."
for REPO in "$CLIENT_REPOS_DIR"/*; do
if [ -d "$REPO/.git" ]; then
echo "Updating: $(basename "$REPO")"
cd "$REPO"
git checkout main
git pull origin main
# Update submodule to latest upstream main commit
git submodule update --remote --merge web/app/mu-plugins/agency-core
if ! git diff --quiet; then
git commit -am "chore(webops): bump agency-core submodule to latest"
git push origin main
echo "SUCCESS: $(basename "$REPO") updated and pushed."
else
echo "NOTICE: $(basename "$REPO") already up to date."
fi
fi
done
055. Enterprise CI/CD Pipeline: Automated Multi-Tenant Deployment
Below is a battle-tested GitHub Actions workflow that clones submodules, caches Composer packages, executes syntax linter tests, and executes an atomic zero-downtime symlink deployment:
name: Enterprise Multi-Tenant CI/CD
on:
push:
branches:
- main
jobs:
build_and_deploy:
name: Build & Atomic Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout Codebase with Submodules
uses: actions/checkout@v4
with:
submodules: 'recursive'
token: ${{ secrets.AGENCY_CI_GITHUB_TOKEN }} # Allows reading private submodules
- name: Setup PHP Environment
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, pdo_mysql, zip, opcache
coverage: none
- name: Cache Composer Dependencies
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Locked Composer Dependencies
run: composer install --no-dev --no-progress --optimize-autoloader --prefer-dist
- name: Compile Front-End Production Assets
run: |
cd web/app/themes/client-child-theme
npm ci
npm run build
- name: Atomic Deployment via SSH
uses: easingthemes/ssh-deploy@v5.0.3
env:
SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_KEY }}
ARGS: "-rltgoDzvO --delete --exclude='web/app/uploads'"
SOURCE: "/"
REMOTE_HOST: ${{ secrets.PROD_SERVER_IP }}
REMOTE_USER: ${{ secrets.DEPLOY_USER }}
TARGET: "/var/www/app/releases/${{ github.run_id }}"
Explore our zero-downtime symlink deployment architecture in Why Your Agency Needs Zero-Downtime Deployment Pipelines.
066. Integrating Multi-Tenant GitOps with Automated Rollbacks via SiteOps
Automating dependencies across 100 repositories introduces a single crucial requirement: automated verification.
What happens if an upstream Composer update in agency-core introduces a breaking change that clashes with Client Cβs specific payment gateway?
This is where SiteOps provides continuous operational protection:
[GitHub Actions Deploys Submodule / Composer Update]
|
v
[SiteOps Executes Automated Canary Checks]
- Multi-Viewport Visual Regression Testing
- Synthetic Checkout User Journey
- PHP Error Log & Telemetry Scan
|
+-----------+-----------+
| |
[Pass: <0.05% Diff] [Fail: PHP TypeError or Visual Break]
| |
[Promote Build] [SiteOps Triggers 30-Second Rollback]
[Isolates Breaking Submodule Commit]
The WebOps Safety Net:
- When a new submodule commit deploys, SiteOps executes automated multi-viewport visual diffing and synthetic checkout canaries.
- If Client Cβs checkout button breaks or throws an error, SiteOps halts the release and restores the previous pre-update snapshot in under 30 seconds.
- The agency engineering team receives an instant alert showing the exact breaking commit, protecting client revenue with zero downtime.
Read how automated visual regression eliminates deployment anxiety in Automating Visual Regression Testing Across 100+ Sites.
077. Actionable GitOps Migration Checklist for Agency Leadership
Follow this 7-step checklist to transition your agency’s fleet to modular Composer and Git submodule management:
- [ ] Step 1: Standardize Directory Hierarchy: Adopt Roots Bedrock or an equivalent structure that isolates public web files from private configuration.
- [ ] Step 2: Externalize Secrets via
.env: Remove all hardcoded database credentials and API keys fromwp-config.php. - [ ] Step 3: Migrate Third-Party Plugins to Composer: Transition core and public plugins to
wpackagist-plugin/*. - [ ] Step 4: Centralize Proprietary Utilities: Move shared custom blocks and mu-plugins into a dedicated
agency-corerepository. - [ ] Step 5: Attach Shared Code via Git Submodules: Link
agency-coreinto client repositories with tracking branches enabled. - [ ] Step 6: Automate Fleet Synchronization Scripts: Deploy automated bash scripts to bump and commit submodule updates across all client repos.
- [ ] Step 7: Enforce Automated Rollbacks with SiteOps: Integrate SiteOps to automatically verify visual layouts and roll back regressions in under 30 seconds.
08Summary & Next Steps: Achieve True Agency Scale with SiteOps
Managing 50+ client websites by manually copy-pasting code and clicking update buttons in WP Admin is an unscalable operational model. By adopting modern GitOps with Composer and Git submodules, forward-thinking agencies create modular, reproducible, and easily maintainable client codebases that scale effortlessly.
Related Resources for Technical Agency Leadership:
- Why Your Agency Needs Zero-Downtime Deployment Pipelines
- How to Eliminate WordPress Staging Environment Drift Using CI/CD
- How to Automate Visual Regression Testing Across 100+ Sites Concurrently
- How Datronix Maintains 150 Client Sites on 2 Hours a Week
Streamline Your Multi-Tenant Operations with SiteOps
Stop wasting hundreds of unbillable developer hours on manual client updates. Automate multi-site deployments, visual regression testing, and 30-second rollbacks across your agency fleet.
09π Start your free trial of SiteOps today and modernize your first three client repositories in under five minutes.
10Frequently Asked Questions (FAQ)
What is the difference between using Composer and Git submodules for WordPress?
Composer manages versioned external dependencies using semantic versioning (^6.7) and locked snapshots (composer.lock), making it ideal for WordPress Core and third-party plugins. Git submodules embed an exact Git commit of another repository directly into your project, making them perfect for proprietary agency mu-plugins and shared parent themes that require bi-directional active development.
Why shouldn’t agencies commit WordPress Core directly to Git?
Committing WordPress Core bloats repositories with thousands of third-party vendor files, creates noisy commit histories, and increases the temptation to make fragile edits directly to core files. Managing WordPress Core via Composer (e.g., roots/wordpress) keeps core files disposable and enables 1-command fleet-wide updates.
How do you avoid blank submodule directories when developers clone a repository?
By default, standard git clone creates empty placeholder directories for submodules. Developers must run git clone --recurse-submodules, or run git submodule update --init --recursive inside an existing working copy to populate all shared submodule codebases automatically.
Can private premium plugins (like ACF Pro) be managed through Composer?
Yes. Agencies can define private package endpoints inside composer.json using licensed download endpoints, or maintain an internal private Composer repository (using open-source Satis or Private Packagist) to distribute premium commercial plugins deterministically across client sites.
How does SiteOps protect multi-tenant agencies when updating shared submodules?
When an updated submodule is deployed across a fleet of client repositories, SiteOps runs automated visual regression testing and synthetic checkout tests across every site. If a shared component breaks a specific clientβs checkout layout, SiteOps automatically rolls back that site in under 30 seconds without impacting other clients.



