MODX Contexts for Multisite: Architecture Decisions and Operational Reality
MODX contexts let one core serve multiple sites, but they share a database, manager, and codebase. This guide covers the minimal design, routing pitfalls, trust boundaries, ACL patterns, cache issues, and conditions that signal you need separate installations instead.
03 Oct 2026, 19:12 UTC

When Contexts Make Sense — and When They Don’t
MODX Revolution’s context system lets a single core installation serve multiple distinct websites, each with its own resource tree, system settings, and domain mapping. This works well for organizations managing a handful of related sites — marketing microsites, regional variants, or brand subsidiaries — where shared content blocks, user accounts, and a unified manager interface reduce overhead. It stops making sense when tenants need true isolation, independent deployments, or separate scaling.
Requirements That Fit the Model
- Two to ~ten sites sharing a codebase, database, and manager access
- Content editors who work across sites but need per‑site permissions
- Common snippets, plugins, and templates reused with minor variations
- Single team responsible for updates, backups, and security patches
If any site requires its own PHP version, custom extensions, or database schema changes, contexts cannot accommodate that — they share one core/ directory and one MySQL/MariaDB database.
Smallest Suitable Design: One Context Per Site
Create exactly one context per website. Do not split a single site into multiple contexts for “sections” (blog, shop, docs) — that adds routing complexity without isolation benefit. Each context needs:
- A unique
key(e.g.,site_en,site_de) http_hostandsite_urlcontext settings matching the public domain- Its own root resource (the homepage) and resource tree
- Optionally, a dedicated media source with
basePath/baseUrlscoped to the context
Shared assets (global snippets, chunks, system plugins) live in the default web context or a dedicated “shared” context. Site‑specific overrides go in each context’s namespace.
Routing: The Gateway Decision
MODX does not route by hostname automatically. You must choose one mechanism:
| Method | Where It Runs | Typical Use Case |
|---|---|---|
| ContextRouter plugin (or similar) | MODX OnHandleRequest event | Simple setups, all domains on one vhost |
Nginx/Apache map or rewrite rules | Web server layer | High traffic, need to avoid PHP boot for wrong host |
| Cloudflare Workers / edge logic | CDN edge | Multi‑region, want routing before origin |
Common breakage: A plugin checks $_SERVER['HTTP_HOST'] but the request arrives via a load balancer sending X-Forwarded-Host. The plugin sees the balancer hostname, matches no context, and falls back to web — serving the wrong site. Fix: read X-Forwarded-Host (validated against a trusted proxy list) in your routing logic.
Trust and Data Boundaries
Contexts are an organizational boundary, not a security boundary. All contexts share:
- The same manager users and passwords
- The same database (tables prefixed per context only for resources, not users/sessions)
- The same PHP process and
core/codebase - The same snippet/plugin execution environment
A compromised manager account, a malicious snippet, or a runaway query in one context affects every site. If you host a client’s site alongside your own, a vulnerability in their custom snippet can exfiltrate your data. For true tenant isolation, use separate MODX installations (containers or VMs).
Access Control: Least Privilege Per Context
MODX’s ACL system (User Groups → Roles → Context Access Policies) is granular but easy to misconfigure. Do not rely on the default “Editor” role — it grants access to all contexts. Instead:
- Create a user group per context:
Editors_site_en,Editors_site_de - Assign a role with a context‑specific policy:
ResourceEditPolicyrestricted to that context’s resource tree - Add users only to the groups matching sites they should edit
- Test by logging in as a restricted editor — they should see only their context’s resources in the tree
Verify: In the manager, switch to the user’s context via the context dropdown (top‑left). If resources from other contexts appear, the policy is too broad.
Operational Checks That Catch Drift
MODX caches context settings aggressively (in core/cache/context_settings/ and the main config.cache.php). After any change to context settings, ACLs, or routing rules:
# From MODX root (requires shell access, same user as web server)
php core/cache/clear_cache.php
# Or via manager: Manage → Clear Cache → All
Diagnostic routine when a site serves wrong content or 404s:
- Confirm the incoming
Hostheader matches a context’shttp_hostexactly (includingwwwvs non‑www) - Check
core/cache/logs/error.logfor “Context not found” or routing plugin errors - Clear cache, then request the homepage with
curl -I https://site.example/— verifyX-Powered-By: MODXand correct HTML - If still wrong, temporarily add
$_SESSION['modx.context.key'] = 'site_en';inindex.phpbefore$modx->initialize()to force a context and isolate whether routing or resource resolution is at fault
Failure Modes and Their Symptoms
| Failure | Symptom | Root Cause | Mitigation |
|---|---|---|---|
| Stale context cache | New site_url ignored; old domain redirects persist | Settings cached in context_settings/; not cleared after change | Automate cache clear in deployment pipeline |
| Gateway misroute | All domains show same homepage (usually web) | Plugin/web server rule matches wrong host or falls back silently | Log matched context key at OnHandleRequest; alert on unexpected fallback |
| Shared snippet context leak | Snippet reads $modx->getOption('site_url') and gets wrong site’s URL | Snippet uses global setting instead of $modx->context | Audit snippets for $modx->context usage; enforce in code review |
| Database contention | One site’s heavy query slows all contexts | Single DB, no query isolation | Monitor slow query log; move high‑load sites to separate installs |
| Media source collision | Editors upload to wrong site’s asset folder | Default media source not scoped per context | Create per‑context media sources with basePath: [[!++context_key]]/assets/ |
Conditions That Change the Design
Move to separate MODX installations (or containers) when any of these appear:
- Tenant isolation required: Client A must not share DB/users/code with Client B
- Independent release cycles: Site A needs MODX 3.1, Site B stays on 2.8
- Independent scaling: Site A gets 10× traffic; you need separate PHP‑FPM pools, DB read replicas, or CDN configs
- Regulatory data residency: Site A’s data must stay in EU, Site B in US
- Custom PHP extensions: One site needs
imagick, anothersodium, host only allows one PHP build
The migration path: export each context’s resources via modx export (or Packager), import into a fresh install, update DNS, and decommission the shared instance. Expect 2–4 hours per site for a clean cutover.
Verification Checklist Before Go‑Live
- Create a second context (
site_test) with distincthttp_host/site_url, point a test hostname (e.g.,test.localvia/etc/hosts), confirm correct homepage renders - Log in as a restricted editor (only
Editors_site_testgroup); verify tree shows onlysite_testresources - Change a context setting (e.g.,
site_name), clear cache, confirm change appears on frontend without manager reload - Break the gateway rule temporarily (rename plugin or comment Nginx map); request a known hostname and observe fallback — should be a 404 or explicit error, not silent wrong‑site render
Document the routing mechanism, cache‑clear procedure, and ACL group names in your runbook. Contexts are convenient until they’re not — know the exit criteria before you commit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.