Publication Type: Technical Review (Whitepaper) & Architectural Roadmap (RFC)
Version: v1.2.0
Audience: Senior PHP Developers, System Architects, Open Source Contributors, and AI Agents
Author / Founder: Ali Çömez (slaweally)
Technology Stack: PHP 8.4+, Symfony 7.4 LTS, Doctrine ORM, AssetMapper, Tailwind CSS Standalone Binary
Introduction: Theory, Practice, and the Pain of "Reinventing the Wheel"
When starting a new project in the modern web ecosystem, developers always find themselves on a double-edged sword. On one side are classic CMS platforms (WordPress, Drupal) packed with ready-made plugin and theme systems, yet burdened with clumsy database schemas, mountains of technical debt, and security weaknesses. On the other side are pure modern frameworks (Symfony, Laravel) that force you to start from scratch—rewriting auth, ACL, file management, and admin panels on every project and burning time in the process.
Building your own MVC stack from zero may sound romantic, but it is not rational by today's standards. A framework is not just routing and controllers; it carries a massive "invisible iceberg" of security (CSRF, XSS, SQLi prevention), Dependency Injection (DI) Container management, and the HTTP layer.
CPalius was born to bridge these two worlds—to free developers from reinventing the wheel while giving them a world-class, optimized, secure, and extensible foundation. This document is a detailed account of CPalius's technical journey: from the cleanest skeleton install, through a "never-crashing" operating-system architecture, to the path from a CMS toward an Enterprise Application Framework.
Section 1: "Pristine Root" and Folder Architecture
In a standard Symfony project, as third-party packages and recipes are installed, the project root turns into a dump of files and folders. The developer's own code mixes with the framework's system files. From day one, CPalius declared war on that chaos and adopted the "Pristine Root" policy.
We isolated the entire architecture so that the root retains only four fundamental folders that make clear what lives where, plus the .env file and composer.json.
Folder Hierarchy
CPalius/ (Root)
|
|-- cp-core/ # === KERNEL & SYSTEM SPACE ===
| |-- bin/ # Console command tool (bin/console)
| |-- config/ # Core configuration (bundles, packages, routes)
| |-- migrations/ # Core database migration files (tracked in Git)
| |-- src/ # Core PHP classes (App\ namespace)
| `-- var/ # Cache, logs, and SQLite files (writable, gitignored)
|
|-- cp-includes/ # === DEPENDENCIES SPACE ===
| `-- vendor/ # Composer packages (Symfony and libraries)
|
|-- cp-content/ # === USER & DEVELOPER SPACE ===
| |-- config/ # Config Sync area (infrastructure shipped as YAML)
| |-- modules/ # Independent modules/plugins (Modules\ namespace)
| |-- themes/ # User interface themes
| `-- translations/ # Global UI translations (tr, en)
|
|-- public/ # === WEB ROOT === (the only publicly exposed folder)
| |-- index.php # Front Controller (single entry point)
| `-- assets/ # Compiled / symlink'ed frontend assets
|
|-- .env # Environment configuration
`-- composer.json # CPalius custom path mappings
Bending Symfony Flex's Internals
To make this layout work, we pushed Symfony Flex and Composer's configuration capabilities to their limits. We configured composer.json so Flex would follow our folder structure: the vendor-dir, bin-dir, and the extra block parameters (config-dir, src-dir, var-dir, public-dir, and the runtime project_dir / dotenv_path).
Technical Note: Changing only config.vendor-dir is not enough. Commands inside Symfony Flex (cache:clear, assets:install) resolve paths from parameters under the extra block—not from Composer's standard config. Without those parameters, the system crashed at compile time. This guarantees that all Flex recipes install directly under cp-core.
Section 2: "Core Never Dies" Architecture
When building a CMF, the worst nightmare is poorly written or broken code from the user space (cp-content/modules) locking up the entire system. Because PHP has no true OS-level sandbox, CPalius developed a multi-tier Active Defense and Quarantine Line.
Defense Line 1: Solving the Chicken-and-Egg Problem (bundles.php)
When Symfony boots, it first reads config/bundles.php. At that stage the database connection, service container, and autoloader are not fully up yet. If we tried to read module active state directly from the database, the system would lock before it could even boot.
Solution: When a module is activated or deactivated, CPalius generates a static, PHP opcache-friendly array file at cp-core/config/active_modules.php. bundles.php only reads that file; the core bundles are always present and each declared module class is added only when class_exists() succeeds.
Defense Line 2: Runtime Isolation (Kernel::boot Override)
A class physically existing (class_exists) does not mean that module's code runs without errors. A TypeError or RuntimeException thrown inside the module's own boot() method can crash the entire system. To prevent that, Kernel::boot() is overridden so every module-level boot process is wrapped in a try/catch (\Throwable) shield; a failing module is written to the quarantine log while the core keeps running.
Defense Line 3: Compile-Time Lock Protection & Dry-Run
If a plugin's services.yaml has invalid YAML syntax or a wrong Dependency Injection (autowire) definition, the Symfony container cannot compile. The system then crashes, and you cannot even run cp:module:deactivate from the terminal.
Solution: An isolated Dry-Run check is integrated into module activation. The module class is written temporarily, a fully isolated subprocess is started with symfony/process, and it runs cache:clear --no-warmup → lint:yaml → lint:container. On a non-zero exit code the main process restores the file in a finally block, cancels activation, and permanently quarantines the module. The file is never left in a broken state.
Defense Line 4: Isolated Route Loader
In standard Symfony, a syntax error in a module's routes.yaml crashes the whole system at route-loading time. SafeModuleRouteLoader loads each module's routes inside an isolated try/catch block, so a faulty module is skipped while the rest of the system stays fully available.
Section 3: Hybrid Data Model and High-Performance Flat Index System
Both classic approaches to content management are problematic:
- EAV (Entity-Attribute-Value) Model (Drupal): Every new field opens a new database table. Fetching a page with 15 custom fields requires 15 JOIN queries; the database locks up.
- Postmeta / Serialized Model (WordPress): All custom fields live row-by-row in a single meta table. Filtering and sorting become a performance disaster.
CPalius Hybrid Data Model: Frequently queried, filtered, and indexed core fields (ID, Title, Slug, Type, Status, Locale, Dates) are stored as real database columns. All dynamic, flexible, project-specific content fields (body text, featured image, gallery fields, SEO data) live in a single SQL json column (data).
The Indexing Deadlock Across SQLite, MySQL, and Postgres
If you want to query dynamic JSON data at the database level, SQLite, MySQL, and Postgres require completely different SQL syntax. Building indexes on generated columns is also extremely fragile with Doctrine ORM schema tools; Doctrine tries to drop those indexes on every schema update.
Solution: NodeFieldIndex and the Dynamic Indexing Engine
CPalius solves this with a Flat Field Index table and a Doctrine Event Listener. A standalone index table carries type-appropriate columns (value_string, value_int, value_decimal, value_datetime) for queryable dynamic fields, each backed by a compound index on field_name.
According to the developer-defined QueryableFieldsRegistry configuration, when a Node is saved or updated, the NodeIndexListener automatically parses the JSON data column, clears old indexes, and writes them idempotently to the helper table on postPersist / postUpdate.
Regardless of which database engine is used, JSON-backed data can then be queried at lightning speed through indexes with a single standard SQL/DQL query via NodeRepository::findByIndexedField() (type, locale, fieldName, value, valueColumn, operator).
Section 4: Multilingual Structure and Composite Uniqueness Constraints
When multilingual (i18n) support is bolted on later, it collapses the entire data model. CPalius baked multilanguage into the core from day one.
Composite Unique Constraints (Uniqueness Guarantee)
In a multilingual setup, classic unique: true constraints lock the system. For example, Turkish /tr/hakkimizda and English /en/hakkimizda must be allowed to exist at the same time. A single global unique slug constraint blocks that.
CPalius Solution: We use composite uniqueness constraints in the database schema:
- Routing uniqueness: The same slug must be unique only within the same locale:
UNIQUE(slug, locale). - Translation group uniqueness: Within the same translation group, only one piece of content may exist per locale:
UNIQUE(translation_group_id, locale).
The same guarantee is carried to four more entities (categories, tags, menu_items, forum_sections) via UNIQUE(translation_group_id, locale), so the database itself enforces "one record per locale per translation group" even under concurrent requests.
Operator UI catalogues
AACP and Studio chrome is bound to ICU YAML under cp-content/translations and each module's Resources/translations. Sidebar labels and #[CpAdminMenu] attributes use translation keys. Hardcoded Turkish (or English) operator strings do not live in PHP, Twig chrome, or JavaScript.
Section 5: From "CMS" to "Application Framework"
CPalius is not merely a content management system (CMS); it is a flexible application platform that can power Car Dealership, Travel Agency, Staff Management, or CRM/ERP systems. To enable that shift, we defined two classes of entities:
| Aspect | Content Entities (Node) | Business Records (Resource) |
|---|---|---|
| Conceptual mapping | Page, Post, Listing showcase, Blog | Vehicle, Invoice, Reservation, Staff |
| Traits | Has slug, multilanguage, publish status, SEO. | No slug, no locale, no publish; has a state machine (workflow). |
| Common ground | Same Capability model, same Twig components, same CLI management, same Config Sync. | |
The #[CpResource] Revolution
To bring coding business processes down to seconds, we designed a custom PHP Attribute. With a single annotation we wire a raw Doctrine class in the database to the full power of the platform:
#[CpResource(
name: 'vehicle',
module: 'oto-galeri',
capabilities: ['create', 'edit', 'delete', 'view'],
auditable: true,
multiTenant: true,
workflow: 'vehicle_lifecycle'
)]
#[ORM\Entity]
class Vehicle
{
private ?int $id = null;
private ?string $plate = null;
private ?string $brand = null;
private ?int $price = 0; // Stored in cents (never Float!)
}
Thanks to this single attribute:
- Dynamic capabilities are registered automatically into the role/capability matrix (
vehicle.create,vehicle.edit, etc.). - Automatic CRUD forms and list screens are derived dynamically from this definition.
- For SaaS projects, multi-tenant isolation (
multiTenant: true) is applied automatically in the background. - Every change on the entity is recorded instantly in the version history (audit log).
The content platform: fields, revisions, workflow and row-level access
A hybrid data model is only half the story. Around it CPalius grew the contracts that turn a table into an application: custom fields, revisions, an editorial state machine, display modes, text formats and per-row permissions. Each was designed against a specific weakness in the system that leads its category.
Field API
Field types, widgets, formatters and per-field settings, attached to any entity that implements FieldableInterface — Node and User today, anything carrying #[CpEntityType] tomorrow. Values live in the entity's JSON column, with a flat index table for the fields that are queried. Entity references resolve in batches rather than one row at a time.
The trap being avoided is WordPress's postmeta and the classic EAV table: both make "give me every post where price > 100" a join storm. The flat index keeps the queryable subset in real columns while everything else stays in JSON and costs nothing until it is read.
Display modes that the API also honours
Drupal's Manage Display is the mature reference, and it has one structural weakness: it is theme-only. Define a "teaser" and the JSON:API output ignores it — you write the shape twice, and the two drift. In CPalius a single EntityDisplayRegistry feeds both the Twig renderer and the API serialiser, so a view mode is defined once and is consistent in both. A test holds that property.
Revisions, moderation and workflow
Moderation state is a separate column from publication status, because "in review" and "unpublished" are different facts that a single field would conflate. The workflow engine underneath is a plain state machine over any subject, defined in YAML: places, transitions, and an optional capability per transition. Content moderation is its first consumer, not its definition — an installation whose places are quote → approved → invoiced uses the same engine and the same screens.
Text formats: filtering on the way in, not only on the way out
Drupal's Filter API is the reference and filters only on output, which leaves the database a store of unsanitised markup. Turn a filter off and old markup comes back to life. CPalius applies an allowlist on save and transformation on output. A deny-list — script, style, form, iframe-on-save — is not overridable by any format, Full HTML included. Permission is re-checked at write time, so a POST from a user who has since lost full_html is rejected rather than trusted. Markdown escapes raw HTML before rendering.
Row-level access without a rebuild
Drupal's node access grants are the reference implementation and carry four known costs: a realm/gid model that is a second mental system next to ordinary permissions; an expensive rebuild whenever the logic changes, which silently returns wrong results if skipped; core support for nodes only; and no validation that a granted permission actually exists.
CPalius answers each one. Grants are expressed in the same capability namespace as everything else, so there is no second model. Every grant is independently correct the moment it is written — there is no rebuild to forget. The entity type is a string, so any #[CpEntityType] participates. An unknown capability is refused rather than stored, so a typo cannot silently grant nothing. On the query side it compiles to a single EXISTS subquery, not a per-row check.
The honest gap: there is no browser UI for sharing a single record with a single user. Drupal core does not have one either.
Section 6: Security and Performance Constitution
Security and performance are not polish added at the end of a project; they are the system's foundational building blocks.
1. Capability-Based Access Control (CBAC)
Standard Symfony ROLE_ADMIN or ROLE_USER approaches are clumsy and hard to extend later. In CPalius, role names are never checked in code; dynamically generated capabilities are always checked instead via denyAccessUnlessGranted('system.module.manage').
- Roles = Config (YAML): Role definitions and their capability matrices live as YAML under
cp-content/config/sync/and travel with Git. - Users = Content (DB): Real user records stay in the database and are never exported.
2. SaaS Data Leak Protection (Automatic Tenant SQLFilter)
In multi-tenant (SaaS) systems, the biggest security hole is a developer forgetting to append WHERE tenant_id = ? to a query. In CPalius that risk is eliminated at the platform level: a Doctrine SQLFilter injects the tenant constraint into every query for entities marked multi-tenant.
3. N+1 Query Guard (Dev-Mode N+1 Guard)
To stop N+1 query mistakes—the most common cause of database bloat and slowness—a Doctrine DBAL middleware fires only in the dev environment. If the number of queries to the same table in a single HTTP request exceeds a set limit, it throws MaxQueriesExceededException immediately. Developers cannot ship code to production until they fix that error locally. The chain is real at the DBAL layer: QueryCounterConnection, Driver, Statement, and TableParser.
4. Voter-to-SQL Conversion
List screens must not run a PHP voter per row (that is an N+1 of authorization). QueryScopeApplier turns the caller's .own / .any capabilities into a Doctrine WHERE before the query hits the database.
5. Zero-Trust Payloads and Core XSS Sanitization
Inbound HTTP bodies are mapped to strict DTOs and validated before they reach controllers. Rich text destined for the database is sterilized by RichTextSanitizer (Manifesto Law 5.3). SchemaOrgBuilder emits JSON-LD (BlogPosting, WebPage) from the same hybrid JSON payload.
6. Optimistic Concurrency
High-write paths use optimistic locking instead of table locks, so concurrent mutations isolate without deadlock theater. Config (roles, capabilities) stays in Git-tracked YAML; users stay in the database—Drupal-style config sync via Symfony TreeBuilder.
Section 7: Platform Extensibility — API, Hooks, Cron, Plugins
Since the first whitepaper draft, four extension backbones landed in core. Each carries the same Core Never Dies armor: a failing extension is quarantined; the kernel stays up.
Cryptographic REST API Gateway
A single /api/{path} wildcard is bound at compile time by ApiRegistrationPass to service methods marked #[CpApi]. Authentication uses the X-CP-API-KEY header, SHA-256 hashes, and timing-safe hash_equals(). Fail-closed: an invalid key never reaches the target method. Each endpoint runs in its own try/catch; a crashing method cannot 500 the gateway.
Dual-lane isolated Hook engine
Cotonti-style Hooks/{hook_point}.php files run inside a bound Closure so they cannot leak $this. Symfony-style #[CpHook] services are collected by HookRegistrationPass. A throwing hook is written to the quarantine log; the page still renders.
Unified hybrid Cron engine
CronManager merges three sources into one list: cp_cron_jobs rows, #[CpCronJob] methods, and Hooks/cron.{job}.php files. Jobs run in isolated subprocesses and may only execute cp:* commands via CronCommandWhitelist. Operators can inspect and “Run now” from /aacp/cron.
Module-independent Plugin layer
Services marked PluginInterface are collected by PluginRegistry; enablement lives in PluginToggleRepository. A module can toggle optional widgets without deactivating the whole module.
Lazy settings
#[CpSetting] keys are loaded through SettingsRegistry on demand—one query when asked, not a boot-time dump of every key.
WordPress-style module packages
A first-party module may ship Resources/config/importmap.php and contributions.yaml. ModulePackageContract validates the on-disk layout; operators can upload a ZIP from AACP. Homepage modes, portal feed blocks, schema types, and the account post-login landing are declared in contributions—core does not hardcode forum_index or blog_index.
Migrate: moving an installation off another CMS
Nobody adopts a framework they cannot move their data into. CPalius ships a migration engine in core and the foreign-system readers in a module — core writes CPalius, the module reads somebody else's schema. Five source systems run on one engine: WordPress from an export file; XenForo, MyBB and Joomla straight from a database; and CSV.
Designed against the reference implementation
Drupal's Migrate API is the standard to beat, and it has four costs. There is no dry run — you learn what it will do by letting it do it. A migration is a soup of YAML plugin identifiers, untyped strings whose typos surface halfway through a run. Core cannot run a migration on its own; a contributed tool is required, and the core UI only handles Drupal-to-Drupal. And rollback is only as good as whatever the id map happened to record.
CPalius inverts each one. Dry run is the default and produces the same counters as the real thing, so --apply is a deliberate act. Transforms are plain PHP, which the IDE completes and the static analyser checks. The runner is in core. Rollback is exact, because the map is written by the runner itself after the destination write succeeds — not before, and not by the migration.
Four guarantees
- Idempotent. A unique key on
(migration_id, source_id)is the guarantee — the database, not bookkeeping, stops a row being imported twice when two runs overlap. An unchanged row is skipped, a changed row is updated in place. - Per-row isolation. One malformed record fails alone; its neighbours import.
- Ordered by declaration. Dependencies are resolved topologically, and a cycle or an unknown dependency is refused rather than silently skipped.
- Streaming. The WordPress reader walks the export with
XMLReader; WordPress's own importer loads the whole file into memory. Database sources page by keyset, so a table that grows during the run cannot cause a skipped page.
What the schemas actually teach you
The interesting work is never the pipeline, it is the source. MyBB stores categories and forums in one table separated by a single character, and a link is a forum with a linkto value — trusting the type column would replace somebody's link with an empty section. Its closed column can hold moved|tid, so comparing it to "1" reads a moved thread as open. Joomla randomises its table prefix at install, its category id 1 is a nested-set anchor rather than a category, and its article body is split across two columns that must be rejoined or every long article is silently truncated.
These are recorded because they are the difference between an import that runs and an import that is correct.
The limit of the evidence
The drivers were written from documented schemas and are tested against fixtures built to match them. That proves the mapping. It does not prove compatibility with every real installation — version drift, plugin columns and the oddities a ten-year-old forum accumulates are real. The first run against a real database is still a real test, which is exactly why dry run is the default and why failures are reported with the source row id.
Section 8: AACP Command Desk, Studio, and Recovery
The Safe Mode / Recovery Console promised in the original roadmap is production code. AACP is the system management center, not a decorative admin skin.
Recovery Console (/aacp/recovery)
The route is intentionally PUBLIC_ACCESS in security.yaml. Authorization is a timing-safe hash_equals() against AACP_RECOVERY_TOKEN in .env. It issues no Doctrine queries, so it still works when the database is gone. An empty token seals the door (fail-safe).
Live command desk (/aacp)
The dashboard polls /aacp/system/metrics for PHP version, load, request time, memory, OPcache hit rate, database telemetry, queue, cron last-run, and module quarantine. Cache Rebuild and OPcache flush are one-click, CSRF-protected actions. Content KPIs live on Studio /admin, not on AACP—AACP is operations, Studio is the editorial command desk.
Studio dashboard
Each first-party module contributes a StudioDashboardStatsProvider (Blog, Forum, Media, Menu, Roadmap, SEO). Studio is a Drupal-style command desk: counts, shortcuts, and homepage portal layout (block order, bilingual copy in portal.{locale}.yaml).
Quarantine, localization, performance backends
/aacp/quarantine is a read-only view of module_quarantine.log. TranslationManager edits core and module ICU YAML with atomic .tmp + rename() writes. The Performance screen tests Redis, Memcached, Varnish, Nginx PageSpeed, and CPalius Origin Cache; the enabled flag is set only after a successful probe—never a fake-green toggle. /aacp/backup is the Backup Management desk (capability system.backup.manage): database dump, files ZIP, or full archive, with download and delete.
Operating an installation: updates, diagnosis, queues and logs
Most of a system's life is spent after it is built. These are the contracts that address that half.
cp:update — one ordered, resumable pipeline
Pending migrations, then core update hooks, then each module's own upgrade step, then configuration import, then a cache rebuild. Drupal's update.php and WordPress's wp core update-db do the schema part but leave data fixes to a separate mechanism. Here an update hook is a typed interface with a ledger, versioned so that skipping several releases still applies them in release order.
Resumability is in the design rather than bolted on: the ledger entry is written after the hook returns, so an interrupted hook runs again instead of being assumed done. A corrupt ledger reads as empty, never as "all finished". One failing hook does not stop its neighbours; only a migration failure halts the pipeline. --dry-run reports the whole plan without touching anything, which is what you run on production first.
The gap it closed was mundane and expensive: a module whose manifest version advanced only ran its upgrade step when it was manually deactivated and reactivated — and the operator had no way to know which modules needed it.
cp:doctor and cp:debug
CPalius discovers most of itself through attributes — #[CpHook], #[CpCronJob], #[CpResource], #[CpEntityType], #[CpFieldType]. That buys a module a great deal of reach for very little ceremony, and it has one cost: a typo at a hook point is not an error, it is silence. cp:debug answers "did mine register?" without making you add a breakpoint; cp:doctor answers "is this installation in the state its own code expects?".
Both are read-only, both emit JSON for CI, and cp:doctor has a failure threshold — with an invalid threshold refused rather than silently disabling the gate.
Two queues, reported separately
Mail and notifications run on Symfony Messenger, which brings native retry and failure transports. Webhooks stay on the platform's own AsyncJob table, where the SSRF guard and signature path are already mature. The admin screen reports both as separate numbers and never adds them into one total that would describe neither.
Logs that survive the thing they are logging
Application logs, mail logs and security telemetry are three tables, not one. Audit entries record entity diffs; telemetry records security events; the watchdog records application faults. Compressing them into a single table would have made each one worse. The log handler is buffered, flushed on terminate, and never throws — a logger that can take down the request it is describing is a liability.
Section 9: Origin HTML Cache and Performance Inventory
CPalius Origin Cache is a first-party HTML cache that does not require Redis, Varnish, or a CDN. It sits beside those backends and can run on plain shared hosting.
How a hit is served
Enabled state is a disk sentinel under public/page-cache/. Apache rewrite in public/.htaccess serves page-cache/{path}/index.html for anonymous GET requests with no query string and no session cookies (PHPSESSID, REMEMBERME). PHP still writes and, for query-string variants, reads through OriginCacheWriter / OriginCacheReader.
What is stored
HTML is optionally minified. Linked CSS/JS can be copied and minified; images can be derived as WebP. An optional HTML shield rewrites markup. TTL, exclude paths (/aacp, /admin, /login, APIs, profiler), minify, asset/image compression, and shield are performance.cpalius.* settings. Cron cpalius.origin_cache.purge drops expired files every 15 minutes. Cache Rebuild also purges origin HTML.
Invalidation
OriginCachePurger runs on Blog post/category/tag writes (including scheduled publish), Forum topic mutations, Roadmap entry saves, and Menu saves—so editorial changes do not leave stale HTML on disk.
AACP Performance panel
The dashboard Performance section inventories every backend the PHP process can see:
- Origin Cache: HTML page count, disk bytes, recent paths with size and mtime.
- Redis:
DBSIZE(key count) andINFO memory(used_memory). - Memcached:
curr_itemsandbytes(plus hit count when the daemon answers). - OPcache: cached scripts, used memory, hit rate.
- Varnish / PageSpeed: enablement and TTL / last probe. Object counts need
varnishstator the PageSpeed admin on the host; PHP cannot invent those numbers.
Probes are isolated and cached for a few seconds so a hanging Redis cannot 500 the dashboard.
Section 10: Backup Management
Disaster recovery is a first-party AACP desk, not a third-party plugin. Capability system.backup.manage gates /aacp/backup. The admin role inherits it via *; editors do not see the menu.
Three archive types
- Database: a gzipped SQL dump written in PHP through Doctrine DBAL.
mysqldumpis not required, so Laragon on Windows works the same as Linux hosting. - Files: a ZIP of the whole CPalius tree, including
cp-includes/vendorand the entirepublic/directory (uploads, compiled assets, themes, front controller). Out: every.env*file,.git,node_modules, Symfony cache, Tailwind compile cache, sessions, Origin Cache HTML, and nested archives undercp-core/var/backups/. - Full: the files ZIP plus
database.sql.gzpacked inside the same archive.
Archives are named cpalius-{db|files|full}-YYYYMMDD-HHMMSS.{sql.gz|zip} and live under cp-core/var/backups/ (already gitignored). Download uses /aacp/backup/archive/{stem} (no .sql.gz / .zip in the URL) so nginx static-file locations cannot intercept the request; Content-Disposition still sends the real filename. Delete is CSRF-protected. Path traversal is rejected by an allowlist on the filename. CLI cp:backup:create is on the cron whitelist so operators can schedule dumps without a web timeout.
v1 does not overwrite a live tree from the browser. Restore is a staging-first operator step: unpack files beside a fresh install, apply SQL on a copy of the database, then swap. Putting restore behind a one-click AACP button would violate Core Never Dies.
Section 11: Security Telemetry and IP Control
AACP records request telemetry without turning the public site into a dark-pattern tracker. The master switch is telemetry.security_enabled and defaults to off.
Visitor mode (default)
The dashboard shows page views, unique IPs, top paths, and a traffic chart. No threat feed, no Ban IP. Suitable for hosting the marketing site without a WAF console in the operator’s face.
Security mode
When enabled, TelemetrySubscriber on kernel.terminate persists a row to cp_system_telemetry_logs: IP, user, method, URI, user-agent, severity, event type, threat score, and JSON details. ThreatAnalyzer scores SQLi, XSS, scanner, path traversal, and login noise. The live feed polls JSON; a details modal opens the row; Ban IP is offered only for critical / threat severities.
IP ban
IpBanService writes cp_banned_ips. BannedIpSubscriber rejects banned clients before the rest of the stack spends budget. A cron task purges old telemetry rows so the table cannot grow without bound.
Section 12: First-Party Modules, Media Pipeline, and Deep Localization
Blog, Media, Menu, Forum, Roadmap, Pages, and SEO are reference modules: they exercise API, Hook, Cron, Plugin, Settings, contributions, and Studio stats the same way a third-party module must.
Blog sits on Node::type = post with categories, tags, scheduled publish (PublishScheduledPostsTask), GET /api/blog/posts, sidebar hooks, and Schema.org BlogPosting. Pages sits on Node::type = page; field groups use fg- slugs and the public route is page_show at /{_locale}/{slug}. Menu is WordPress-style drag-and-drop; items reference nodes loosely (no hard FK) so soft-delete stays honest. Roadmap is a native feed that can also pull related blog and forum activity onto the portal. SEO contributes sitemap sources and JSON-LD.
Forum engine
Hierarchical boards reuse the Node tree; prefixes, a CSRF-protected report/moderation queue, ranks/badges, and a 20/80 postbit layout (Golden Ratio) run on the same capability model as the rest of the CMF.
Media pipeline (Manifesto Law 3.3)
Asset is independent of Node. Flysystem storage plus sha256 dedup. ImageProcessor (GD) builds crop or fit derivatives into public/uploads/cache/; cp_thumb accepts an Asset, id, storage key, or /uploads/... URL. A failed derivative returns the original URL (fail-soft). purge() drops all sizes when the source changes.
Deep localization
Each row lives in its locale and joins siblings via translation_group_id UUID, enforced by UNIQUE(translation_group_id, locale) (also on categories, tags, menu items, forum sections). LocaleSwitchService builds the counterpart URL; missing siblings fall back to that locale’s home—never a 404. AACP has no locale prefix; panel locale is the cp_locale cookie. Translation files are written atomically (.tmp + OS rename()).
Zero Node.js in core UI
Admin and developer chrome use AssetMapper + the Tailwind standalone binary. There is no Node.js build for those surfaces. Public theme CSS may still be a static theme asset.
How the claims in this document are verified
Every architectural claim above is only worth the gate that keeps it true. CPalius runs the following on every change; the build fails rather than warns.
- PHPStan level 6 across core, modules and tests, with a tracked baseline. Errors are fixed, not suppressed; when a baseline entry stops matching it is deleted, which is how dead code gets noticed.
- Two test suites. Unit tests run without a database; integration tests boot a real kernel against a real MySQL schema, reset between tests by reading the database rather than the mapping — a distinction that used to make the suite non-deterministic.
- A third suite for modules, so a module's behaviour is proven in the same run as the core it depends on.
- Static lint gates: container (dev and prod), YAML, Twig, and a code-style pass.
cp:doctoras a build step, not only a support tool: pending migrations, quarantined modules, capabilities held by roles but never registered, translation-key parity between catalogues, environment misconfiguration, security posture and unrun update hooks.
Two gates that exist because something escaped
The translation check compares key sets, not line counts. Two catalogues with identical line counts can carry different keys, and the missing one renders as a raw identifier in production.
cp:doctor was written after an incident, not designed in the abstract. Five migrations sat unapplied across several development sessions and nothing broke loudly, because the features that needed the new tables were written to degrade quietly. The only symptom was security telemetry writing nowhere. An unapplied migration is therefore rated HIGH: the danger is not that it fails, it is that it does not.
A diagnostic tool must survive the installation it is diagnosing. If one check throws, the run continues and the exception becomes a finding. A doctor that dies on the first real problem is useless at exactly the moment it is needed.
Benchmark: CPalius against Drupal, TYPO3, WordPress and ProcessWire
A framework is chosen on evidence, not on adjectives. This chapter is the scorecard the project maintains against the four systems it is most often compared to, and it is published unedited — including the rows where CPalius is behind, and the one row nobody has measured yet.
Scale. D absent · C primitive or partial · B incomplete · A mature · A+ best of its class · A++ ahead of the field.
Columns. CP CPalius · DR Drupal 10/11 · T3 TYPO3 v13 · WP WordPress 6.x · PW ProcessWire 3.x.
| # | Capability area | CP | DR | T3 | WP | PW |
|---|---|---|---|---|---|---|
| 1 | Crash-proof core / module isolation | A+ | B | B | C | B |
| 2 | Security by default (CBAC, tenant, N+1 guard, XSS) | A++ | A | A | C | A |
| 3 | Hybrid data model + flat index | A | B | B | C | A |
| 4 | Entity API (one fieldable/revision/i18n/access contract) | B | A+ | B | C | A |
| 5 | Field API (type + widget + formatter + settings) | B | A+ | A | C | A |
| 6 | Query API (EntityQuery / selector) | B | A | B | B | A+ |
| 7 | Row-level access (grants + query alter) | A+ | A+ | B | C | B |
| 8 | Taxonomy (fieldable vocabulary + term) | A++ | A+ | A | A | A |
| 9 | Revisions + moderation + workflow | B | A+ | A | B | C |
| 10 | Typed entity lifecycle events | A+ | A | B | C | A |
| 12 | Display modes | A | A+ | A | A | C |
| 13 | Cache API (tags/contexts) + reverse-proxy purge | A++ | A+ | A | C | B |
| 14 | Origin / full-page HTML cache in core | A | A | A | D | B |
| 15 | Text formats + filter pipeline (per role) | A+ | A+ | B | B | B |
| 16 | Token + pattern-based path alias | A+ | A+ | A | B | B |
| 17 | Configuration management (config as code) | A | A+ | B | D | B |
| 18 | Multilingual — content + interface | A | A+ | A+ | C | A |
| 19 | Media pipeline + responsive image + oEmbed | B | A | A | A | A |
| 20 | Async queue + notification primitive | A++ | B | B | B | C |
| 21 | Multisite / domain / organisation | C | A | A+ | A | B |
| 22 | REST / JSON:API / headless auth | B | A+ | B | A | B |
| 23 | Hook / plugin ergonomics | B | A | B | A+ | A |
| 24 | Cron / scheduled tasks | A | A | A | C | B |
| 25 | Module packaging + lifecycle + one-click install | A | A | A | A+ | B |
| 26 | Migrate / CMS-to-CMS data transfer | A+ | A+ | B | B | C |
| 27 | Admin UI + recovery console | A+ | A | A | A | A |
| 28 | Security telemetry + IP ban in core | A | C | C | C | C |
| 29 | Backup in core | A | C | C | C | C |
| 30 | DX: maker + doctor + test kit + developer docs | A | A | B | B | B |
| 31 | Release policy + BC guarantee + LTS | D | A+ | A+ | A++ | A |
| 32 | Installation experience (the five-minute rule) | B | B | C | A++ | A |
| 33 | Documentation | C | A+ | A+ | A+ | A+ |
| 34 | CI / automated quality gate | A | A+ | A | B | B |
| 35 | Package ecosystem + distribution channel | D | A+ | A | A++ | B |
| 36 | Vulnerability disclosure + CVE process | B | A++ | A+ | A | B |
| 37 | Performance evidence (benchmarks) | D | B | B | C | A |
| 38 | Accessibility (WCAG) — admin UI | ? | A+ | A | A | C |
| 39 | Observability (logs / metrics / health) | A | A | A | C | B |
| 40 | Database portability | C | A+ | A | C | C |
Row 11 is deliberately absent. It used to read "Views / no-code listing", and it was removed rather than renumbered: a site-builder UI serves websites, not the CRM, ERP and panel installations this core exists for. The gap in the numbering is the decision, left visible.
Where CPalius is ahead
- Crash-proof core (row 1). Four lines of defence — a static
active_modules.php, atry/catchshield around module boot, a dry-run lint before activation, and a route loader that survives a broken module. A faulty module is quarantined; in Drupal it is a white screen. - Security by default (row 2). Capability-based access with unknown-means-deny, a tenant SQL filter, a development N+1 tripwire, voter-to-SQL scoping, nonce-based CSP, a blocking WAF, CIDR and timed IP bans, flood and brute-force defence, HIBP password policy, TOTP two-factor in core, encrypted setting secrets and a scored
cp:security:audit. In Drupal each of those is a separate contributed module; in WordPress several are paid plugins. None of them offers one screen and one posture score. - Taxonomy, cache tags, async queue (rows 8, 13, 20). Cache invalidation needs no rebuild step: saving an entity emits a typed lifecycle event and the tag index purges itself. Drupal's equivalent is mature but its access system famously requires a rebuild that silently returns wrong results when skipped.
- Backup, telemetry, recovery console (rows 27–29). All in core, and
/aacp/recoveryworks without a database — the screen you need precisely when the database is the thing that is broken.
Where CPalius is behind, stated plainly
- Release policy and package ecosystem (rows 31, 35 — both D). There is no semantic-versioning promise, no backwards-compatibility document and no module registry. The update machinery exists (
cp:update); the policy around it does not. - Documentation (row 33 — C). README and SECURITY.md exist. A developer handbook does not. This is the single lowest score on the card and it is not an oversight, it is unfinished work.
- Performance evidence (row 37 — D). The architecture is built for speed and no benchmark has been published. Until one is, the claim is an argument, not a measurement.
- Database portability (row 40 — C). Every migration is raw MySQL DDL. The ORM layer is portable; the installation is not.
- Accessibility (row 38 — unmeasured). The admin UI has never been audited against WCAG. Reporting it as "unknown" is more useful than guessing a grade.
Section 13: Future Roadmap and Technical Consultation (RFC)
The core security, performance, and architectural backbone of CPalius is complete. Since this whitepaper's first draft the following have shipped: AACP Safe Mode / Recovery Console, the isolated Hook system, the unified Cron engine, the REST API Gateway, the fourth defense line (SafeModuleRouteLoader), the on-demand image pipeline (cp_thumb), the #[CpResource] audit log, Studio dashboard stats, CPalius Origin Cache, the AACP performance inventory, optional security telemetry with IP ban, WordPress-style module packages and the contribution catalog (homepage, portal, schema, account landing), the Pages module, operator UI catalogue binding, and AACP Backup Management (cp:backup:create).
In upcoming development sprints we will build the following systems:
- Workflow & State Machine: A mechanism that manages transition processes for business records such as invoices, vehicles, and reservations (
draft → preparation → sold) via YAML definitions, and automatically ties every transition to the audit log and notification queue.CpResource::$workflowis already declared. - Messenger Async Queue: Wiring a real transport (Doctrine/Redis) so email and notification work move onto an asynchronous queue.
symfony/messengeris installed and visible on the AACP desk; no active transport is bound yet.
Questions and Feedback (Community Consultation)
To refine this architecture further, we welcome feedback from you valued developers on these topics:
- Flat Field Index Model: How do you think this model—designed for SQLite, MySQL, and Postgres compatibility—will perform at massive data volumes? Should the index table be partitioned?
- Config Sync Approach: What do you think of our idea to emulate Drupal's config sync system using Symfony's TreeBuilder?
- Zero Node.js Stance: Will our decision to use AssetMapper + Standalone Tailwind in the developer and admin panels constrain us when writing highly complex frontend components later?
We eagerly await your ideas, critiques, and architectural suggestions. With the strength of the community, CPalius will become the most solid application framework.