Zoosper CMSDocumentationGitHub

Architecture

Zoosper is an API-first modular CMS built on Marko components and PHP 8.5.

Runtime

The application loads layered configuration, discovers modules, composes services, loads module routes and executes middleware before dispatching controllers. Site context is resolved once per request and carried through the request path.

Modules

Modules own routes, controllers, services, migrations, schema declarations, permissions, Admin menus, Admin assets, templates, translations and extension contracts. Installed Composer modules opt in explicitly through package metadata.

Configuration

Module defaults are loaded below project overrides. Secrets remain environment-owned. Settings can expose configuration without becoming the source of truth for every value.

Persistence

Repositories own database access. Migrations are module-owned and discovered through the module registry. SQLite is supported for local development and automated fresh-install proof; MySQL is the production target.

Presentation

Latte is the current default template engine, not a platform-wide restriction. The architecture remains API-first and template-engine integration should remain replaceable.

Security

Authentication, CSRF, route permissions, two-factor authentication, security headers, sanitisation, rate-limiting seams and audit logging are explicit runtime boundaries.

Logging boundary

Logging is owned by the standalone zoosper/logger package. Core and feature consumers use the native Zoosper logger boundary, while the package delegates physical writes to Marko FileLogger with DailyRotation. Module config/logging.php contributions remain discoverable and retain their logical channel and legacy filename identities. Root Composer owns zoosper/logger; the package owns marko/log and marko/log-file.

Feature lifecycle APIs

Feature modules own lifecycle routes and response mapping. Page archive, restore and guarded permanent deletion use Page-owned application/domain boundaries; Auth owns only PAT identity and scope validation.

Editor-controlled style-smuggling boundary

Editor-generated HTML does not accept <style> elements or inline style attributes. The production HTML Purifier allowlist excludes style surfaces. The explicitly enabled basic fallback removes those surfaces and neutralises javascript:, vbscript:, and data: values in quoted href and src attributes. Content Security Policy and web-server hardening remain independent defence layers.

Security closure boundaries

Recognised environments are local, development, testing, staging and production; empty or unknown values fail boot. Staging and production require secure sessions and enforced salted rate limiting in both HTTP and console service composition. Logger redaction covers credential-bearing keys and Zoosper PAT-shaped values. PAT issue and revocation require audit infrastructure. Successful PAT use updates last-used metadata at most once per five-minute database interval. Session /api/v1/me is read-only; bearer /api/v1/token/me remains the stateless integration identity endpoint.

Template identifier security boundary

Template identifiers are validated before theme overrides, module view lookup, extension-driven engine selection, or legacy PHP template execution. Identifiers must be normalised relative forward-slash paths. Absolute paths, traversal segments, null bytes, backslashes, duplicate separators, malformed module names, and unsupported segment characters fail closed.

Authentication rate-limit execution boundary

Authentication rate limiting uses one Auth-owned execution service across HTML Admin login, API login, password-reset requests and two-factor challenges.

AdminAuthenticationRateLimiter owns:

HTTP layers remain transport adapters. The Admin middleware performs route selection and generic HTML 429 mapping. API and two-factor controllers preserve their own response formats. Password-reset requests preserve their neutral public response. No HTTP adapter recreates the policy, hashing, persistence or reporting stack.

Disabled mode remains lazy and does not require a salt or create storage. Report-only mode records the underlying decision but returns an allowed decision to the caller. Enforcing mode returns the stored decision unchanged.