Files
Florian Wilde f432122abf feat: add a dedicated Security Auditor agent with veto power
Security was one section among seven in the QA Engineer. That is structurally
weak: the agent that wants to recommend "production ready: YES" also judged
security, and as a subsection it competes for attention with acceptance
criteria, regression and test authoring.

The new agent:
- has no Write/Edit tools, so "never fix what you find" is enforced by the tool
  list rather than by instruction — it cannot launder a finding into a patch
- reports PASS / FAIL / UNVERIFIED, and UNVERIFIED must reach the spec; an audit
  that omits what it could not check reads as all-clear
- audits against .claude/rules/security.md, the same standard the developer
  agents are held to, so every mandatory item gets a verdict
- holds a veto: BLOCK stops /deploy regardless of functional test results

/qa now runs both agents in parallel and keeps the verdicts separate. The QA
Engineer is explicitly forbidden from speaking for security.
2026-08-09 01:22:05 +02:00

4.7 KiB

name, description, argument-hint, user-invocable
name description argument-hint user-invocable
deploy Deploy the Docker Compose stack to a server, with pre-flight checks and rollback. PROJ-X or 'production' true

Deploy

Self-hosted Docker Compose. There is no Vercel, no managed platform. The stack runs one app container; TLS and routing are handled by a reverse proxy already present on the host.

1. Gate

  1. Read features/INDEX.md — what is being shipped
  2. Read the feature spec's QA section

No QA section: → "Für PROJ-X gibt es noch kein QA-Ergebnis. Führe erst /qa aus." → Stop. Critical or High bugs open: → name them and stop. Those get fixed first. Security verdict is BLOCK, or missing entirely: → stop. A feature without a Security Auditor verdict has not been security-reviewed, no matter how green the functional tests are. UNVERIFIED security items: → list them and ask the user explicitly whether to ship with that surface unchecked. Do not decide this silently on their behalf.

2. Pre-flight

Run and show the results — do not proceed on a failure:

npm run lint
npm run typecheck
npm test
npm run build

Then check:

  • Every env var used in code is in .env.example with a dummy value
  • No secrets in the diff (git diff --cached)
  • Pending migrations are committed
  • Everything committed and pushed to the remote the server pulls from

Port audit — blocking. Only the app service may publish a port, and only on loopback:

docker compose config | grep -B8 "published:"

Any service other than app with a published port, or any binding to 0.0.0.0, stops the deploy. A published database port puts the data on the public internet and bypasses ufw, because Docker writes its own iptables rules. Fix compose.yaml before continuing.

3. First deployment only

Walk the user through this once. It happens on the server, over SSH.

Prerequisites the user provides manually: a server with Docker and the Compose plugin, a reverse proxy (nginx, Caddy, Traefik) terminating TLS, and a DNS record pointing at it.

git clone <repo-url> /srv/<app-name> && cd /srv/<app-name>
cp .env.example .env

Then edit .env:

  • NEXT_PUBLIC_APP_URL — the real public URL. If auth is used, this must match the domain exactly: the WebAuthn relying-party ID comes from this hostname, and a mismatch makes every passkey fail without a useful error.
  • APP_PORT — the port the reverse proxy forwards to
  • APP_BIND — leave at 127.0.0.1 so the container is not exposed directly
  • BETTER_AUTH_SECRET — openssl rand -base64 32, unique per environment
docker compose up -d --build
curl -fsS http://127.0.0.1:${APP_PORT:-3000}/api/health

Point the reverse proxy at 127.0.0.1:$APP_PORT and make sure it forwards X-Forwarded-Proto and X-Forwarded-Host, otherwise redirects and WebAuthn origin checks break.

Passkeys need HTTPS in production — confirm TLS works before testing auth.

4. Subsequent deployments

On the server:

./scripts/update.sh

It pulls the branch, rebuilds, restarts, waits for the healthcheck, and rolls back to the previous commit if the new container never turns healthy. --force rebuilds without a new commit; --branch <name> deploys a different branch.

If a data layer exists, migrations run before the new container serves traffic — see docs/DEPLOYMENT.md.

5. Verify

  • Public URL loads over HTTPS
  • /api/health returns {"status":"ok"}
  • The deployed feature works end to end
  • Sign-in works and offers passkey first (if auth exists)
  • No console errors, no errors in docker compose logs app
  • Security headers survived the proxy (DevTools → Network → Response Headers)

6. Bookkeeping

  • Add a Deployment section to the spec: URL, date, commit SHA
  • features/INDEX.md → Deployed (write, then re-read to verify)
  • Tag: git tag -a v1.X.0-PROJ-X -m "Deploy PROJ-X: [name]" and push it

Rollback

scripts/update.sh rolls back automatically on an unhealthy container. Manually:

git reset --hard <previous-sha>
docker compose up -d --build

Common failures

Symptom Cause
Build works locally, fails in Docker A dependency sits in devDependencies but is needed at build time
Env var undefined at runtime Not in .env on the server, or missing NEXT_PUBLIC_ for client use
Container healthy, proxy returns 502 Proxy points at the wrong port, or APP_BIND is not reachable from it
Passkeys fail after deploy NEXT_PUBLIC_APP_URL does not match the real domain, or TLS is missing
Redirect loop Proxy not forwarding X-Forwarded-Proto

Commit

deploy(PROJ-X): Deploy [feature name] to production