Files
project-manager/docs/setup.md
T
aneurinandClaude Sonnet 5 5c8f2dbf07 Un-hard-wrap all Markdown documentation
Every prose paragraph and list item was manually wrapped at ~80-100
columns; joined each back into a single line. Headings, table rows,
and fenced code blocks are untouched -- tables already had one row per
line, and wrapping inside a code fence is the code's own formatting,
not something this applies to.

Also fixed two pre-existing typos this surfaced (both from wrapping
without leaving the space that was actually intended): a missing space
in "{ challenge_id, options }" and a stray "+ TypeScript" that had
accidentally been written as if it were a new line, in web/README.md
and README.md respectively.

Code comments are explicitly out of scope for this -- left exactly as
they were.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 02:29:08 +01:00

4.9 KiB

Setup & configuration

For the quick version — just running the app — see the root README. This covers the rest: running without Docker, every configuration option, and the test suite.

Run with Docker

The only requirement is Docker with the Compose plugin.

docker compose up -d

This runs a multi-stage build — a Node stage compiles the Vue frontend, then a PHP 8.3 + Apache stage (Alpine-based; apk's own prebuilt packages rather than compiling PHP from source, which is most of why the image is ~90MB rather than several times that) bakes in the PHP source and the built SPA — applies migrations, and serves the whole app at http://localhost:8080: the SPA at / (assets and all) and the REST API under /api (e.g. curl http://localhost:8080/api/health). Unknown paths fall back to the SPA shell for client-side routing.

  • A Mailpit container (the maintained MailHog successor — one ~15 MB Go binary, messages kept in memory) also starts. The API sends all email to it; read it at http://localhost:8025. Set MAIL_TRANSPORT=mail or =smtp (with MAIL_SMTP_*) to send for real.
  • The image is the artifact: PHP source and the compiled frontend are copied in at build time, not bind-mounted. Rebuild to pick up any code change: docker compose up -d --build. For iterating on the frontend, run the Vite dev server on the host instead (see web/README.md).
  • The SQLite database and the generated JWT signing key live in the storage named volume, mounted at /var/www/storage, so they survive docker compose restart / down + up.
  • docker compose down -v removes the volume and gives you a clean database.
  • Override settings via the environment or a .env file in the repo root (Compose substitutes APP_DEBUG, JWT_SECRET, JWT_TTL — see docker-compose.yml).

Run without Docker

Requires PHP 8.1+ with the pdo_sqlite and mbstring extensions, plus Composer. On Fedora:

sudo dnf install php-cli php-pdo php-mbstring composer

Setup

composer install
cp .env.example .env      # optional; sane defaults are used without it
composer migrate          # creates storage/database.sqlite and its tables

Running

composer serve            # http://localhost:8080  (php -S localhost:8080 -t public)

Any web server can serve the API as long as the document root is public/ and unknown paths fall through to public/index.php. public/.htaccess also serves a built frontend from public/ (copy web/dist/ there) and only falls back to index.php for /api and when no index.html is present.

The frontend itself still needs its own toolchain — see web/README.md.

Configuration

All settings are optional environment variables (read from .env or the real environment). See .env.example.

Variable Default Purpose
APP_DEBUG false Include exception details in error responses
DATABASE_PATH storage/database.sqlite SQLite file location
JWT_SECRET auto-generated into storage/secret.key Token signing key
JWT_TTL 86400 Token lifetime in seconds
APP_ALLOW_REGISTRATION true When false, a magic link is only ever sent to an existing address — an unknown one is silently ignored, so no new accounts get created
MAGIC_LINK_RESEND_SECONDS 60 Minimum gap before a magic link can be resent to the same address (sign-in or email-change). Docker Compose overrides this to 0, so links resend immediately in development
APP_URL http://localhost:8080 Base URL used to build magic links (http://localhost:5173 for a host npm run dev)
WEBAUTHN_RP_ID APP_URL's host Passkey relying party ID (domain). Must be localhost or a real domain over HTTPS — a LAN IP won't work
WEBAUTHN_RP_NAME Projects Passkey relying party display name, shown in the browser/OS prompt
MAIL_TRANSPORT mail mail (PHP mail()), smtp, or log (append to a file)
MAIL_FROM / MAIL_FROM_NAME no-reply@todo.test / Projects Envelope sender
MAIL_LOG_PATH storage/mail.log Where log transport writes
MAIL_SMTP_HOST / _PORT / _USERNAME / _PASSWORD / _ENCRYPTION — / 587 / — / — / tls Used only when MAIL_TRANSPORT=smtp

Standalone, SMTP is opt-in and the API otherwise falls back to PHP's mail(). Under Docker Compose the default is MAIL_TRANSPORT=smtp pointed at the bundled Mailpit container (mailpit:1025, no auth/TLS); open http://localhost:8025 to read what was "sent".

Tests

composer install         # installs phpunit (require-dev)
vendor/bin/phpunit

Each test run applies every file in migrations/*.sql to a fresh SQLite database, so it always exercises the current schema from scratch.