Overview
This page explains the architectural idea of CPalius: the core behaves like an operating system, modules run in user space. When a program in user space crashes, the operating system stays up; CPalius gives the same guarantee.
Layers
cp-core/ KERNEL area: Symfony application, services, AACP/Studio templates
cp-includes/ Composer dependencies (vendor)
cp-content/ USER area
modules/ modules (with their own migrations, permissions, menus, translations)
themes/ themes
config/sync/ configuration that travels with Git
public/ web root (single entry point, compiled assets)
Namespaces: App\ belongs only to the core, Modules\ belongs to modules. A module cannot define a class in the App\ namespace.
The core never dies
A module can crash, contain a syntax error or blow up at boot. In that case the core and AACP must stay up. This is not a wish but a contract enforced by the laws below.
Law 1: Static, safe boot
During boot, the database is not touched and the service container is not trusted. Active modules live in cp-core/config/active_modules.php as class-name strings; bundles.php reads only this light file. This way the system can boot even if the database is down or a module corrupts the container.
Law 2: Pre-build protection
Before a module is added to active_modules.php, the activator (cp:module:activate and the panel use the same service) runs cache clearing, a YAML check and a container check in an isolated child process. If it fails, the module is permanently quarantined and is not activated. This is not a warning but a hard gate.
Activation also checks the requires, conflicts and core_version matrix in module.json; a mismatch is reported with a readable message.
Law 3: Safe mode and recovery console
AACP is rendered without depending on custom modules or themes. /aacp opens even if all modules are quarantined. Even if the database is down, the recovery console at /aacp/recovery brings up a minimal core with a token in .env; you can disable broken modules and clear the cache without SSH.
Law 4: Every request moment is an isolated extension point
- The primary path is
#[CpHook]. The hook manager wraps every call (file-based and attribute-based) intry/catch. The error is written tocp-core/var/log/module_quarantine.logand the request continues. - A module can also bind directly to Symfony
kernel.*events with#[AsEventListener]. This is allowed but does not grant immunity: at build time every event listener belonging toModules\*classes is rewritten to run throughModuleEventListenerGuard. - Result: a runtime exception from a module cannot bring a page it does not own (especially
/aacp) down to a 500. If you find such a path, it is a bug in the guard, not the module breaking the rule.
You can see the quarantine status in AACP → System → Quarantine.
Law 5: Package isolation
Every module package is checked by code (not by review) before activation:
- It cannot write to or reference
cp-core/orcp-includes/(rejected at the zip entry level; including path traversal). - It cannot declare the
App\namespace in its own files. - It must carry
module.json, a bundle class whosebundlefield matchesModules\<Directory>\<Directory>Module, andInstall/ModuleInstaller.php.
A failing package is rejected at the start of activation; it never reaches active_modules.php.
Law 6: Migrations must be idempotent
A module's Resources/migrations/*.sql files are marked "applied" by file name only; there is no content checksum. As a result:
- Every statement must survive running more than once:
CREATE TABLE IF NOT EXISTS,ALTER TABLEguarded byinformation_schema,INSERT IGNORE. - Once a file is released and applied somewhere, its content is effectively frozen. To fix something, release a new, separately named file.
cp:doctorchecks whether the tables an applied migration claims to have created really exist; it reports drift but does not repair it. Repair is done with an explicitUpdateHookInterface.
Module lifecycle
discovery → (install | activation pre-check) → active → [upgrade] → disabled → (uninstall)
- Discovery:
cp-content/modules/<Name>/module.jsonis found and parsed. - Activation: dependency check, child-process test, writing to
active_modules.php. - Install hook:
ModuleInstallerruns; pending SQL migrations are applied. The installed version is stored incp_settings(no extra table needed). - Upgrade: when the module's
module.jsonversion increases,cp:updateruns the module'supgrade()operation and its new migrations. - Disabling: removed from
active_modules.php; data is not deleted. - Uninstall (
--purge): the uninstall hook runs and the module's data is deleted.
Details: Module skeleton.
Plugin versus module
A module is a separate installation unit (its own folder, manifest, migrations). A plugin is a sub-feature inside a module; it can be switched on and off without disabling the whole module (AACP → System → Plugins).
The core's service families
The core is split into topic-based packages under cp-core/src/Core/. Each is covered in detail on the following pages:
| Topic | Page |
|---|---|
| Node, Resource, revision, trash, workflow | Content model |
| Capability, voter, query scope | Permission system |
| Field types, widgets, formatters | Field system |
| Locale, translation manager | Multilingual support |
| Hook, cron, queue, events | Hooks, cron, queue |
| Settings, config sync | Settings and configuration |
| REST API, API key, webhook | API and webhooks |
| Media, storage, CDN | Media and storage |
| Cache, performance | Cache and performance |
| Search, SEO, sitemap | Search and SEO |
| Email, notifications | Email and notifications |
| Backup, doctor, recovery | Backup and maintenance |
| WAF, password, 2FA, CSP | Security |
| Signed updates | Update system |
Asset and theme isolation
- Core and panels: Symfony AssetMapper and the standalone Tailwind binary. There is no Node.js dependency.
- Themes: completely separate from the core. A theme developer can use Vite or esbuild;
theme.jsondeclares only the compiled output entries (dist/). CPalius does not compile theme assets, it only serves the compiled output. Theme templates also run inside request isolation like everything else; a template error is caught at the response layer.
Details: Theme contract.
Performance budget
Performance is not something thought of afterwards but a budget checked with every change:
- N+1 ban in development: if the same query passes a threshold (for example 10) in a single request, an exception is thrown; it forces you to write a
fetch join. - Permissions are turned into SQL in lists (
QueryScopeApplier); a voter is not run per row. - Flat field index: fast filtering on JSON fields.
- Sessionless anonymous traffic: a precondition for the cache to be efficient.