# M01 — Monorepo Skeleton & Toolchain > Fresh Claude Code agent prompt. You are starting from an empty repo (or a repo with only `SPEC.md` and an empty `PROGRESS.md`). > Estimated effort: medium (mostly boilerplate; the goal is solid foundations). ## Mission Create the monorepo layout, set up the toolchain (composer, npm, PHPUnit, PHPStan, php-cs-fixer), and produce buildable Docker images for both the `api` and `ui` containers. Both containers start under compose, return placeholder healthchecks, and `migrate` runs an empty Phinx migration set and exits 0. **No business logic in this milestone.** ## Before you start 1. Read `SPEC.md` end-to-end once. Even if it feels long, do it — every later milestone assumes you understand the architecture. 2. Then re-read these sections carefully: §1 (Project Goals), §2 (Tech Stack), §3 (Architecture), §10 (Docker), §11 (Project Structure), §14 (Coding Conventions). 3. Verify the working tree: ```bash ls -la # should see SPEC.md and PROGRESS.md git status # clean ``` 4. Confirm tooling: `docker --version`, `docker compose version`, `php --version` (8.3+), `composer --version`, `node --version` (20+), `npm --version`. ## Tasks Execute in this order. Commit nothing until acceptance passes. ### 1. Repo skeleton Create the directory structure exactly as in SPEC.md §11. Empty placeholders are fine where files come later (e.g. `api/src/Domain/.gitkeep`). ### 2. Root files - `.env.example` — every env var from SPEC.md §9, grouped into "Shared", "API container", "UI container" sections, with comments. - `.gitignore` — sensible defaults: `vendor/`, `node_modules/`, `public/assets/`, `.env`, `.phpunit.cache/`, `.phpunit.result.cache`, `data/`, IDE files. - `docker-compose.yml` — exactly as in SPEC.md §10. - `compose.scheduler.yml` — exactly as in SPEC.md §10. You don't need to make it work end-to-end yet; you only need it to be valid YAML. - `README.md` — minimal: project name, one-paragraph description, "see SPEC.md and milestones/ for details". ### 3. `api/` subproject - `api/composer.json` — Slim 4, doctrine/dbal, robmorgan/phinx, monolog, php-di, vlucas/phpdotenv (dev), guzzlehttp/psr7. Dev: phpunit ^11, phpstan ^1.10, friendsofphp/php-cs-fixer ^3. Set `"type": "project"` and PSR-4 autoload mapping `App\\` → `src/`. - `api/public/index.php` — Slim app bootstrap. Two routes for now: `GET /healthz` returning `{"status": "ok"}` (JSON), and a 404 fallback. Wire structured JSON logging via Monolog to stdout. - `api/config/settings.php` — builds a config array from environment variables. Don't read `.env` in production; do read it in `development` via phpdotenv. - `api/config/phinx.php` — Phinx config that reads the same env vars. Migrations dir `db/migrations`, seeds dir `db/seeds`. Both SQLite and MySQL adapters configured. - `api/db/migrations/.gitkeep` and `api/db/seeds/.gitkeep`. - `api/bin/console` — minimal Symfony Console app with one command: `db:migrate` that delegates to `vendor/bin/phinx migrate`. Make it executable. - `api/docker/entrypoint.sh` — dispatcher script that switches on `$1` (`api` default → starts FrankenPHP serving `public/`; `migrate` → runs migrations and exits). Make it executable. - `api/docker/Caddyfile` — FrankenPHP/Caddy config serving on `:8081`. Configure `/internal/*` location with the `remote_ip` matcher from SPEC.md §6 (you don't need any internal routes yet, just the protective Caddy match). - `api/Dockerfile` — multi-stage as described in SPEC.md §10, `dunglas/frankenphp:1-php8.3-alpine` base. Install `pdo_sqlite`, `pdo_mysql`, `mbstring`, `intl`, `opcache`, `bcmath` extensions. - `api/phpunit.xml` — testsuite includes `tests/Unit` and `tests/Integration`. - `api/phpstan.neon` — level 8 on `src/`. - `api/.php-cs-fixer.dist.php` — PSR-12 + strict types. - `api/tests/Unit/SmokeTest.php` — one trivial assertion (`$this->assertTrue(true)`) so the suite runs. ### 4. `ui/` subproject Same shape, but: - `ui/composer.json` — Slim 4, twig/twig, slim/twig-view, guzzlehttp/guzzle, jumbojett/openid-connect-php, monolog, php-di, vlucas/phpdotenv (dev). No DBAL, no Phinx. Same dev deps as api. - `ui/package.json` — `tailwindcss ^3`, `postcss`, `autoprefixer`, `alpinejs`, `htmx.org`. Build script: `tailwindcss -i resources/css/app.css -o public/assets/app.css --minify`. - `ui/tailwind.config.js` — content paths covering `resources/views/**/*.twig` and `resources/js/**/*.js`. `darkMode: 'class'`. - `ui/postcss.config.js` — autoprefixer + tailwindcss. - `ui/resources/css/app.css` — Tailwind directives only. - `ui/resources/js/app.js` — Alpine + htmx imports. - `ui/resources/views/layout.twig` — minimal HTML skeleton with Tailwind classes, ``, dark-mode toggle button (no JS yet, just the markup). - `ui/resources/views/pages/hello.twig` — extends layout, says "IRDB UI — milestone 1". - `ui/public/index.php` — Slim with Twig. One route `GET /` renders `pages/hello.twig`. One route `GET /healthz` returns `{"status":"ok","api_reachable":null,"last_api_check_at":null}`. - `ui/docker/Caddyfile` — serves on `:8080`. - `ui/docker/entrypoint.sh` — single mode (`ui`). - `ui/Dockerfile` — multi-stage as in SPEC.md §10. - `ui/phpunit.xml`, `ui/phpstan.neon`, `ui/.php-cs-fixer.dist.php`, `ui/tests/Unit/SmokeTest.php`. ### 5. `examples/` and `doc/` placeholders - `examples/scheduler/host.crontab` — a comment-only stub for now (real content lands in M13). - `doc/.gitkeep` — empty (docs land in M13). ### 6. CI (local) Do **not** create any GitHub Actions workflow. CI runs locally on this server (which has Docker installed). Create `scripts/ci.sh` that an operator can invoke manually: - Make it executable (`chmod +x scripts/ci.sh`) and start with `#!/usr/bin/env bash` and `set -euo pipefail`. - Runs each stage in order, fails fast on the first non-zero exit, and prints a clear banner before each stage. - Stages: 1. `api/`: `composer install --no-interaction --prefer-dist`, then `composer stan`, `composer cs`, `composer test`. 2. `ui/`: `composer install --no-interaction --prefer-dist`, then `composer stan`, `composer cs`, `composer test`. 3. `ui/`: `npm ci` then `npm run build`; assert `ui/public/assets/app.css` exists. 4. `docker compose build` from the repo root to verify both images build. - DB driver matrix: accept an env var `DB_DRIVERS` (default `"sqlite mysql"`) and loop the api test stage once per driver, exporting `DB_DRIVER` so phpunit can pick it up. For `mysql`, skip gracefully with a warning if no local MySQL is reachable (this milestone has no DB-touching tests yet, so the loop is mostly scaffolding for later milestones). - At the end, print a green "CI OK" line. On failure, the failing command's exit code propagates. Also add a one-line invocation note in `README.md` under a "Local CI" heading: `./scripts/ci.sh`. ## Implementation notes - **FrankenPHP entrypoints**: the entrypoint script must `exec` the FrankenPHP process for proper signal handling. `exec frankenphp run --config /etc/Caddyfile`. - **Caddy `:8081` for api, `:8080` for ui** — match SPEC.md §10. Don't expose the api on 8080. - **healthz format**: api returns `{"status":"ok"}` for now; full payload (`db`, `jobs`) lands in later milestones. Comment in the code where additional fields will go. - **Service token bootstrap**: do not implement yet. SPEC says the api ensures `UI_SERVICE_TOKEN` exists on startup — that lands in M03 when `api_tokens` table exists. - **Volume mount**: api's `entrypoint.sh` should `mkdir -p /data` before launching, in case the SQLite path is set but the dir wasn't created. - **Composer scripts**: add convenience scripts in each subproject's composer.json: `test` (phpunit), `stan` (phpstan), `cs` (cs-fixer), `cs-fix` (cs-fixer fix). Lets the agent in later milestones run `composer test` instead of remembering paths. ## Out of scope (DO NOT) - Do **not** create any database tables or migrations beyond an empty migrations dir. Tables come in M02. - Do **not** implement any auth, tokens, RBAC, sessions. That's M03/M08. - Do **not** install any deps not listed above without strong reason. Note any addition in PROGRESS.md. - Do **not** write business logic. The api should respond only to `/healthz`. The ui should render only the hello page and `/healthz`. - Do **not** wire OIDC, MaxMind, or any external service. - Do **not** create files in `doc/` beyond `.gitkeep`. - Do **not** touch `SPEC.md`. It is read-only for milestone agents. ## Acceptance Run all of these. Every one must pass. If any fails, fix and re-run; do not commit until all green. ```bash # 1. Static analysis and style cd api && composer install && composer stan && composer cs && composer test && cd .. cd ui && composer install && composer stan && composer cs && composer test && cd .. # 2. Frontend build cd ui && npm ci && npm run build && test -f public/assets/app.css && cd .. # 3. Compose build docker compose build # 4. Compose up — migrate exits 0, api and ui become healthy cp .env.example .env # Set required secrets in .env so containers start (use placeholder values for now): # APP_SECRET, UI_SECRET, UI_SERVICE_TOKEN, INTERNAL_JOB_TOKEN — any 32 hex chars each. docker compose up -d sleep 15 # Migrate must have exited 0: test "$(docker compose ps -a --format '{{.Service}} {{.State}} {{.ExitCode}}' | grep migrate | awk '{print $3}')" = "0" # Healthchecks (poll up to 60s): for i in {1..30}; do curl -sf http://localhost:8081/healthz && curl -sf http://localhost:8080/healthz && break sleep 2 done curl -sf http://localhost:8081/healthz | grep -q '"status":"ok"' curl -sf http://localhost:8080/healthz | grep -q '"status":"ok"' # Hello page renders: curl -sf http://localhost:8080/ | grep -q "milestone 1" docker compose down -v ``` ## Handoff 1. Commit: ``` feat(M01): monorepo skeleton, toolchain, docker compose builds clean - api/ and ui/ subprojects with composer + slim 4 - frankenphp dockerfiles, multi-stage builds - phpunit, phpstan level 8, php-cs-fixer wired into composer scripts - tailwind build pipeline in ui/ - empty phinx migrations directory - scripts/ci.sh runs the full test/lint matrix locally (no GitHub Actions) ``` 2. Append to `PROGRESS.md`: ```markdown ## M01 — Monorepo skeleton (done) **Built:** repo layout per SPEC §11, both Dockerfiles, compose stack, toolchain. **Notes for next milestone:** - DB schema empty; M02 owns all tables and seeds. - `entrypoint.sh` for api supports `migrate` mode and calls `vendor/bin/phinx`. - Healthcheck payloads are stubs; later milestones extend them. - Service-token bootstrap deferred to M03 (needs `api_tokens` table first). **Deviations from SPEC:** none. **Added dependencies beyond SPEC §2:** none. ``` 3. **Stop.** Do not start M02.