A zero-dependency PHP 8.4 content-management framework with a built-in real-time chat, engineered for privacy-first deployments — Tor hidden services, hostile networks, JavaScript-disabled clients — where every feature must work with scripting off and nothing may phone home.
AstrX ships no Composer packages at runtime. The framework, the CMS, the moderation stack, the mail/webmail layer, and the live chat are all pure PHP on top of MariaDB and a handful of standard extensions. There is no build step required to run it, no CDN dependency, and no client-side framework.
- No JavaScript required. Every page, form, moderation control, and the live chat function with scripting disabled. Navigation is server-rendered and Post/Redirect/Get throughout; the chat "streams" by auto-refreshing iframes, not XHR.
- One controller, HTML and JSON. The web view and the JSON API are the
same code path. A page opts into the API with a flag, and per-value
ContextScopetags decide exactly what crosses into JSON — no parallel "API controllers" to keep in sync. - CSS-only themes. A theme is a directory of metadata plus a stylesheet; switching it changes no markup and ships no script.
- Errors are values, not control flow. Operations return a
Result<T>monad carrying typedDiagnosticobjects rather than throwing — all the way to the edge, and into the API envelope. - Zero runtime dependencies, native English/Italian i18n enforced by a parity check, and a tree that is clean at PHPStan level 10.
The same application runs through three front controllers, selected at the web-server level:
- Server-rendered (default) —
public/index.php. Plain, fast HTML with PRG navigation; the mode the whole framework is designed around. - Experimental
/js/browser — a progressive-enhancement client served under/<locale>/js/, with a same-origin API index at/<locale>/js/api.json. Entirely optional; the site is fully usable without it. - Compiled bundle —
php tools/compile.phppacks all ofsrc/and the read-only resources into a singlebuild/astrx.compiled.phpwith an on-demand autoloader (not naive concatenation). Serve it directly viapublic/compiled.php, or benchmark it side-by-side under/compile(public/compile/index.php). Config and uploaded state stay external and mutable. Seedocs/COMPILED_BUILD.md.
Several features — the imageboard, the chat, the bot-trap honeypot, site-wide
search, and the webmail client — are optional modules that a deployment can
turn off without touching core: the module's navigation disappears, its pages
return the themed 404, and its schema can be dropped. Each module self-declares
in a src/AstrX/<Module>/module.php manifest that AstrX\Module\ModuleRegistry
discovers, so adding one is a manifest plus a page tag — core names no module.
php tools/module.php status # each module: enabled/disabled + page count
php tools/module.php disable chat # nav drops, pages 404 — reversible
php tools/module.php purge chat # also drop its tables (destructive)
php tools/make_module.php forum --nav # scaffold a new module
php tools/check_modules.php # CI gate: validate every manifest
Toggle them in resources/config/Modules.config.php. See docs/MODULES.md for
the manifest contract and a walkthrough of writing your own.
A single front controller resolves the request through the router into a page
record, then into a controller. Controllers are constructed by a
reflection-based dependency injector (AstrX\Injector) that autowires services
from their constructor type-hints, and configuration is bound declaratively
through the #[InjectConfig] attribute — a config setter is matched to a key in
a *.config.php file and invoked at wiring time.
Pages live in a database table with a closure table for the hierarchy, are rendered through a Mustache-style template engine (escape-by-default, with path-traversal-guarded partial loading), and can be decorated with per-page meta, robots, keywords, and templates. A navbar builder assembles the menu tree from pinned and grouped entries. Optional modules — news, comments, the API, a feed, mail and IMAP webmail — hang off the same core.
| Concern | Where it lives |
|---|---|
| Front controllers | public/index.php, public/compiled.php, public/compile/ |
| Routing & dispatch | src/AstrX/Routing/, src/AstrX/ContentManager.php |
| Dependency injection | src/AstrX/Injector/ (reflection autowiring) |
| Configuration | #[InjectConfig] → resources/config/*.config.php |
| Templating | src/AstrX/Template/ (Mustache-style, escape-by-default) |
| Result / diagnostics | src/AstrX/Result/ |
| Internationalization | src/AstrX/I18n/, resources/lang/{en,it}/ |
| Auth, sessions, CSRF | src/AstrX/Auth/, src/AstrX/Session/, src/AstrX/Csrf/ |
| Pages & hierarchy | src/AstrX/Page/ (+ closure table) |
| Chat | src/AstrX/Chat/, src/AstrX/Controller/Chat* |
| Admin & moderation | src/AstrX/Admin/, src/AstrX/Controller/Admin* |
| HTTP API | src/AstrX/Api/ |
| Mail & webmail | src/AstrX/Mail/ |
| Themes | src/AstrX/Theme/, resources/template/themes/ |
A real-time chat that needs no JavaScript: an auto-refreshing message stream, guest and member posting, private messages, per-user settings and themes, and a complete moderation surface. Moderators get an in-chat admin panel — live sessions, kick and ban routed through the shared banlist, broadcast, room topic, and bulk clean — plus guest-access modes with optional moderator approval before a guest may post. Abuse control is layered: managed word / link / nick filters with automatic kick, a user report → moderator queue that can turn a reported link into a kick filter in one action, per-identity flood protection with auto-mute, and EXIF-stripped image attachments (uploads are re-encoded through GD, so metadata and polyglot payloads are discarded and only pixels survive). Every limit — retention, message count, refresh interval, upload size and dimensions, capacity — is admin-configurable.
Any page becomes a JSON endpoint by setting api_enabled = 1; the controller
that renders the web view serves the API unchanged. A response carries both
structured data and the fully-rendered html in a single round trip (drop the
HTML with ?html=0). What appears in data is governed by per-value
ContextScope tags, so enabling the API never blanket-exposes a controller's
internals — an enabled page with nothing tagged returns "data": {}.
| Scope | Web HTML | API JSON |
|---|---|---|
WEB_ONLY (default) |
yes | never |
SHARED |
yes | yes |
API_PUBLIC |
no | yes |
API_ADMIN |
no | admin callers only |
Authentication is by astrx_-prefixed bearer key (192-bit, scoped to one user),
an existing session cookie, or anonymous — and permissions and policies apply
exactly as they do on the web. Full reference in docs/API.md.
Passwords are hashed with Argon2id and transparently rehashed on verify; a dummy
verification runs on unknown users to flatten timing. Sessions use 128-byte
CSPRNG identifiers stored as SHA-512 digests, with session data sealed under
AES-256-CTR encrypt-then-HMAC using domain-separated HKDF-derived keys and a
constant-time MAC check. CSRF protection is per-session, single-use, 256-bit, and
compared with hash_equals on every state-changing POST. Authorization goes
through a default-deny Gate with a permission enum and per-resource policies.
The banlist is a first-class identity-ban engine spanning IP/CIDR, email, nick, and user, with penalty rounds and expiry; IPv4 is handled as IPv4-mapped IPv6 so v4 and v6 bans share one code path. An append-only admin audit log records significant admin actions — who did what, to which resource, from which address, and when — across user management, moderation, the banlist, and every configuration save.
Themes are CSS-only. Each lives in resources/template/themes/<name>/ as a
theme.config.php (name, description, author, version) plus a stylesheet;
selecting one swaps the stylesheet without touching markup or shipping any
script. A site-wide default and per-user overrides are set from the admin and
user-settings surfaces.
- PHP 8.4 with
pdo_mysql,gd,fileinfo, andmbstring - MariaDB 10.4+ (or MySQL 8+)
- A web server with the document root pointed at
public/
docker compose up --build
On first boot the MariaDB container runs src/setup/init.sql, which creates the
content_manager database and the application user. Then open public/setup.php
and follow the wizard — it installs the schema from src/setup/tables.sql and
writes the config files under resources/config/.
The init script runs only against a fresh data directory. If you have booted the stack before, reset the database volume first:
docker compose down -v.
- Point the web server's document root at
public/. - Create an empty database named
content_managerand a user that can access it (seesrc/setup/init.sqlfor the grants the Docker path uses). - Visit
public/setup.phpand complete the wizard, or initialise by hand: importsrc/setup/tables.sqland fill inresources/config/PDO.config.php. - Make the upload directories writable by the web server — the chat upload dir
(
upload_dirin the chat config, defaultresources/chat_uploads) andresources/avatar.
The schema is a single file, src/setup/tables.sql; there are no
incremental migration files. Schema changes are folded into that file, and an
existing database is brought forward by applying the delta by hand.
Runtime configuration lives in resources/config/*.config.php. Each file is a
plain PHP array bound to a typed config object via #[InjectConfig], and almost
everything is editable from the in-app admin panels (access rules, captcha,
chat, mail, webmail, themes, system) rather than by hand-editing files. The chat
alone exposes 60+ settings through its admin surface.
Locale catalogs live under resources/lang/<locale>/, split by domain, with a
matching pair for English (en) and Italian (it). tools/check_lang_parity.php
verifies that both locales define exactly the same keys and exits non-zero on any
divergence — run it before committing.
php phpstan.phar analyse # static analysis, level 10, must be clean
php tools/check_lang_parity.php # en / it translation parity
php tools/compile.php # (re)build the single-file compiled bundle
docs/API.md— the HTTP API: opt-in, context scopes, auth, response envelope.docs/COMPILED_BUILD.md— the compiled single-file bundle and how to serve it.docs/PROFILING.md— profiling normal vs. compiled front controllers.
public/ entry points — index.php (dev), compiled.php + compile/ (bundle),
setup.php, avatar/captcha endpoints
src/AstrX/ the framework — Chat/, Controller/, Auth/, Admin/, Http/, Api/,
Template/, Routing/, Injector/, Result/, Mail/, News/, Comment/,
Theme/, Navbar/, Page/, …
src/setup/ init.sql (database bootstrap) + tables.sql (the complete schema)
resources/ templates (Mustache-style + themes/), lang/ (en, it), config/
docker/ Dockerfiles and service config for the compose stack
tools/ maintenance scripts (check_lang_parity.php, compile.php, …)
docs/ API.md, COMPILED_BUILD.md, PROFILING.md