Files
project-manager/docs/architecture.md
T
aneurinandClaude Sonnet 5 f1309b4c10 Rebuild the Docker image on Alpine: ~735MB -> ~89MB
Stage 2 was php:8.3-apache (Debian), which compiles PHP from source
with --with-apxs2 for mod_php -- that base image alone is 719MB of
our 735MB, before any app code. Replaced with alpine:3.24 + apk's own
prebuilt php83/php83-apache2/apache2 packages: same architecture (one
process, mod_php, .htaccess-driven rewriting), no fpm/nginx rewrite
needed.

- docker/apache.conf: rewritten for Alpine's apache2 (mod_rewrite ships
  but isn't loaded by default; a different default document root/log
  paths). Logs redirected to stdout/stderr so `docker logs` still shows
  them -- Alpine's own defaults write to a real file under ServerRoot,
  unlike the official Debian image's symlinked paths.
- docker/entrypoint.sh: su-exec instead of su -- BusyBox's su doesn't
  take the same -c/user argument order as the GNU one the previous
  entrypoint relied on. Also moved earlier in the Dockerfile (with the
  other rarely-changing setup, before COPY . .) so it no longer re-runs
  on every build for a file that essentially never changes.
- Composer's binary is still borrowed from the official composer:2
  image via multi-stage COPY, not apk's own `composer` package, which
  turned out to pull in an entire second PHP interpreter (php85) as a
  dependency just to run itself.
- ext-iconv needed adding explicitly (symfony/polyfill-mbstring depends
  on it; the official Debian image bundles it by default, apk doesn't).

Verified against the real compose stack, not just that it builds: apk
install; composer install; migrations on startup; PHPUnit 88/88 (runs
on the host, but confirms nothing else broke); and by hand, all
through the actual container -- health check, SPA fallback for unknown
routes, static assets served directly, the API's 401 guard, and a full
magic-link -> verify -> JWT -> authenticated project create/list round
trip via the real Mailpit catcher.

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

2.6 KiB

Architecture

A REST API in PHP 8 (Slim 4) over a single SQLite file, plus a Vue 3 + TypeScript PWA frontend. Auth is a bearer JWT, obtained via a magic link or a passkey (see docs/api.md) — there's no session store or cookie.

Backend layout

public/index.php              Front controller
src/bootstrap.php             App wiring and route definitions
src/Support/Config.php        Environment-driven configuration
src/Support/Database.php      PDO/SQLite connection
src/Auth/JwtService.php       Issue/verify JWTs
src/Auth/AuthMiddleware.php   Bearer-token authentication
src/Auth/SessionPayload.php   Shared user + session JSON shape
src/Mail/                     Mailer interface, SMTP/mail()/log transports, EmailVerifier
src/Http/JsonErrorHandler.php Uniform JSON error envelope
src/Http/Controllers/        Request handlers (Auth, EmailVerification, Passkey, Project, Card, CardStatus)
src/Repository/              Database access (User, EmailVerification, Passkey, WebAuthnChallenge, Project, Card, CardStatus)
src/Support/Validator.php    Request-body validation helper
migrations/*.sql             Schema, applied by bin/migrate.php
Dockerfile                   Multi-stage: Node frontend build + PHP 8.3/Apache runtime
                              (Alpine -- apk's own php83/apache2 packages, not a
                              from-source compile; ~90MB image)
docker-compose.yml           Local stack: app (SPA + API) + Mailpit
docker/                      Apache vhost (mod_rewrite, document root) + entrypoint
                              (runs pending migrations, then hands off to Apache)
web/                         Vue 3 + TypeScript + Vite PWA frontend (dev on the host)

A ProjectScopedController base class centralizes "look up a project owned by the caller, or 404" for the controllers that need it (Project, Card, CardStatus). Every table a request can reach is scoped to the authenticated user one way or another — directly (owner_id/user_id) or via a project that is.

Frontend

The Vue/TypeScript PWA lives in web/ and is a separate concern with its own conventions (routing, state, styling, drag-and-drop). See web/README.md for all of that — this document only covers the backend.

Database

One SQLite file, migrated forward-only by bin/migrate.php from migrations/*.sql (each applied file is recorded in a schema_migrations table, so re-running is safe). See docs/setup.md for how to run migrations, and docs/history.md for how the schema and the rest of the app got here.