7.9 KiB
Development Guide
Setup, workflow, building assets, and testing for XAMXAM development.
Requirements
- PHP ≥ 8.4 with
ext-json,ext-openssl,ext-pdo(andext-sqlite3for the local DB) - Composer for PHP dependencies
- Node.js / npm for frontend asset builds (rolldown, lightningcss, biome)
- SQLite3 CLI (for
just query,just init-db)
One-time setup
From the repo root, run the provisioning script:
just provision
This is idempotent and safe to re-run. It:
- Ensures
app/.envhas anAPP_KEY— never overwrites an existing key (it prints a message and keeps the current one so encrypted credentials stay decryptable). - Installs Composer deps (
composer install) and JS deps (npm ci). - Runs the DB schema + migrations (
just migrate). - Checks the first-admin-password state and tells you if
/admin/is still unauthenticated.
If you prefer to run the steps manually, or just isn't installed yet:
composer install # PHP deps (vendor/)
npm ci # JS build deps (node_modules/)
just migrate # create/update the SQLite DB from schema + migrations
just setup exists but its backing script (scripts/setup-dev.sh) is stale
— it still clones the old php-live-reload library and creates legacy
admin/data/ directories. Live-reload now ships inside the app
(app/public/live-reload.php), and assets are built with rolldown/lightningcss,
not the live-reload watcher. Prefer just provision.
APP_KEY and the app/.env file
The app derives DB/storage paths from the filesystem layout, so the public site
and admin panel run without any env file. But Crypto (decrypting the SMTP
password, PeerTube credentials, and Nextcloud WebDAV sync) requires an
app/.env containing an APP_KEY:
APP_KEY=<base64-encoded 32 random bytes>
Generate one manually with:
php -r 'echo "APP_KEY=" . base64_encode(random_bytes(32)) . PHP_EOL;' > app/.env
chmod 600 app/.env
app/.env must never be committed. just provision only creates app/.env
(or appends APP_KEY) when the key is absent — if the value already exists it
is left untouched so existing encrypted credentials (SMTP password, PeerTube
OAuth, Nextcloud WebDAV) remain decryptable. If the file is missing, the public
site still runs but any path that reads encrypted credentials throws
RuntimeException: APP_KEY not found.
First admin login
A brand-new DB seeds site_settings.admin_password_hash as an empty string,
which makes AdminAuth::requireLogin() a pass-through — i.e. /admin/ starts
unauthenticated until a password is set. To secure it, open
http://127.0.0.1:8000/admin/account (or the production admin) and set the
admin password, which stores the bcrypt hash in site_settings.
Project Structure
xamxam/
├── app/ # All application code (the project root on the server)
│ ├── bootstrap.php # App bootstrapping (constants, autoload, config)
│ ├── router.php # Router for the PHP built-in dev server
│ ├── public/ # DocumentRoot (web-accessible only)
│ │ ├── index.php # Front controller / entry point
│ │ ├── request-access.php
│ │ ├── live-reload.php
│ │ ├── admin/ # Admin panel
│ │ ├── partage/ # Student submission via share links
│ │ └── assets/ # Built CSS/JS, fonts, images (dist/ is generated)
│ ├── src/ # PHP classes
│ │ ├── Controllers/ # Request controllers (Home, Search, Tfe, Export, …)
│ │ ├── Form/ # Form helper
│ │ ├── Database.php # DB access + queries
│ │ ├── AdminAuth.php # Session auth for admin
│ │ ├── RateLimit.php
│ │ ├── FilepondHandler.php
│ │ ├── PeerTubeService.php
│ │ └── … # (see list in src/)
│ ├── templates/ # Template partials (public, admin, partials, partage)
│ ├── storage/ # Database + uploads (private)
│ │ ├── xamxam.db # SQLite database
│ │ ├── schema.sql # Base schema
│ │ ├── theses/ tfe/ # Uploaded files
│ │ └── cache/ logs/ # Runtime data
│ └── migrations/ # Migration runner + applied migrations
├── scripts/ # Build / deploy / utility scripts
├── tests/phpunit/ # PHPUnit tests
├── nginx/ # Server configuration + docs
├── deploy/ # Cron configs (backup, cleanup)
├── justfile # Task runner
├── composer.json / package.json
└── TODO.md
Development Workflow
Start the dev server
just dev
This runs a one-shot asset build, applies pending migrations, opens
http://127.0.0.1:8000/ (public) and /admin/ (admin), then starts the PHP
built-in server (php -S 127.0.0.1:8000) with a chokidar watcher that
rebuilds CSS/JS on change.
- Public site:
http://127.0.0.1:8000/ - Admin panel:
http://127.0.0.1:8000/admin/
Stop it with just stop.
If you only need the server without browsers/ui, run just dev-build first,
then start the PHP built-in server manually with the php -d … -S command
shown in the dev recipe (or add a just serve alias locally).
Frontend assets
Source CSS lives in app/public/assets/css/, source JS in
app/public/assets/js/app/. They are bundled and minified into
app/public/assets/dist/ by scripts/build*.mjs (rolldown + lightningcss).
just dev-build # one-shot build (quicker output than full)
just build # full build
just build-css # CSS only
just build-js # JS only
just build-check # verify build output is up to date
There is no live-reloading in production — app/public/live-reload.php only
activates under the PHP built-in server.
Testing
PHPUnit is used. Configuration lives in phpunit.xml, tests in tests/phpunit/
(with tests/bootstrap.php and tests/TestDatabase.php helpers).
just test # run all PHPUnit tests
just test-coverage # run with HTML coverage into coverage/
Linting / formatting
just lint-php # phpstan (static analysis) + php-cs-fixer (coding standards)
just lint-css # biome lint on app/public/assets/css/
just lint-js # biome lint on app/public/assets/js/app/ + scripts/
just lint # all linters
just fix # auto-fix (biome + php-cs-fixer)
Database Operations
just migrate # run pending migrations
just init-db # create/reset DB from app/storage/schema.sql
just reset-db # rm DB + init-db
just query # open an interactive SQLite shell
just backup # SQL dump into app/storage/backup_<timestamp>.sql
just backup-snapshot # WAL-safe hot backup + gzip into storage/backups/
just cleanup-drafts [--no-dry-run] # remove orphaned drafts > 7 days (default)
# OLDER_THAN_HOURS=24 to change the threshold
just cleanup-tmp-uploads [--no-dry-run] # GC abandoned FilePond uploads (>2h or missing session)
# TMP_UPLOAD_MAX_AGE_SECONDS to change the threshold
See database.md for the full schema reference.
Deployment
See deployment.md. Files are pushed to the server with
just deploy (rsync → xamxam:/var/www/xamxam/). There is no git repo on
the remote.
Troubleshooting
just logs # tail the dev error log (error.log)
just stop # kill the dev server / asset watcher
If the browser doesn't hot-reload, confirm the asset watcher is still running
and the rebuild succeeded (just dev-build).