Skip to content

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) in try/catch. The error is written to cp-core/var/log/module_quarantine.log and 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 to Modules\* classes is rewritten to run through ModuleEventListenerGuard.
  • 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/ or cp-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 whose bundle field matches Modules\<Directory>\<Directory>Module, and Install/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 TABLE guarded by information_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:doctor checks 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 explicit UpdateHookInterface.

Module lifecycle

discovery → (install | activation pre-check) → active → [upgrade] → disabled → (uninstall)
  1. Discovery: cp-content/modules/<Name>/module.json is found and parsed.
  2. Activation: dependency check, child-process test, writing to active_modules.php.
  3. Install hook: ModuleInstaller runs; pending SQL migrations are applied. The installed version is stored in cp_settings (no extra table needed).
  4. Upgrade: when the module's module.json version increases, cp:update runs the module's upgrade() operation and its new migrations.
  5. Disabling: removed from active_modules.php; data is not deleted.
  6. 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.json declares 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.
Last updated: 13.10.2026