Architecture
Zoosper is an API-first modular CMS built on Marko components and PHP 8.5.
Project-owned theme roots are resolved from the nearest zoosper/zoosper Composer project rather than from a module package directory, so source and vendor-only artifact layouts share the same frontend and Admin themes.
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.
Zoosper owns the single live HTTP dispatcher. Module route manifests feed that dispatcher, and duplicate normalised method-and-path registrations fail during registration rather than replacing or shadowing an earlier handler. See docs/architecture-decisions/route-registration-ownership.md.
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. Marko 0.11 rotation receives a PSR-20 clock inside the Zoosper logger boundary. 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:
- runtime policy loading
- salted and normalised identity construction
- database attempt recording
- report-only JSONL diagnostic events
- enforcing allow or deny decisions
- successful login and two-factor bucket resets
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.
Release artifact layout parity
Release readiness resolves required Settings, Page and Session files through enabled ModuleRegistry identities, preserving missing-module and missing-file failures in source, modules and vendor layouts. Application-owned assets and foreign-key checks remain unchanged. Session defaults locate the owning zoosper/zoosper Composer project rather than assuming directory depth; absolute and stream storage paths remain unchanged. Artifact acceptance must run without restoring app/ or packages/.
Admin middleware composition
Production application composition refuses to register Admin routes when the module-discovered middleware pipeline is empty. This prevents authentication, CSRF, and other contributed controls from being bypassed by missing composition. The reusable route loader and middleware pipeline retain explicit empty-list support for isolated tests and non-production composition.
API authorization ownership
API routes remain stateless and feature-owned. The route-level public flag means they bypass the Admin session middleware; it does not by itself mean anonymous access. A repository-wide parity contract allow-lists the reviewed anonymous endpoints and requires every other API controller to retain either the session guard or Personal Access Token authenticator as its authorization owner.
API object authorization
Page, Menu and URL Rewrite API resources are scoped to the Site resolved from the current request. Cross-Site identifiers resolve as not found. Role, Site, Theme and Media are application-level management resources rather than request-Site resources; their APIs remain protected by narrow PAT scopes and the corresponding current-user management permission. Repository-wide parity contracts preserve both classifications.
API response data
First-party API controllers construct response arrays from explicit field maps rather than serialising domain objects wholesale. Credentials, token hashes, protected two-factor material and private storage paths are excluded. External API Grid integrations likewise validate upstream schemas and map only the fields required by the local Grid.