---
title: "Review - Bootstrap-wizard plan v2 met webresearch"
status: draft
date: 2026-05-13
source_plan: "/Users/janpetervisser/.claude/plans/als-ik-een-nieuwe-virtual-turtle.md"
previous_review: "docs/recommendations/bootstrap-wizard-plan-review-2026-05-13.md"
---

# Review - Bootstrap-wizard plan v2 met webresearch

## Conclusie

De eerdere aanbevelingen zijn grotendeels verwerkt, maar nog niet "goed" genoeg om dit plan direct naar implementatie te brengen. V2 lost de meeste oude schema-, enum-, status- en path-safety punten op papier op. De grootste resterende fout is dat het plan twee executor-modellen tegelijk beschrijft: eerst `scrum4me-docker` als deterministic runner, later de Next.js app als executor met een fire-and-forget background promise. Kies er een.

Mijn advies: maak de app niet de lange-running executor. Gebruik voor MVP een aparte `bootstrap-service` of breid de bestaande docker-runner expliciet uit met een veilig secret-contract. Vercel/Next fire-and-forget is te broos voor clone, file mutation, GitHub repo-create en push.

## Zijn de eerdere aanbevelingen verwerkt?

| Reviewpunt | Status | Oordeel |
|---|---:|---|
| Deterministic runtime ipv `model: null` | Ja | Goed concept, maar nog te veel gekoppeld aan `JobConfig` als de app uiteindelijk executor wordt. |
| Worker-eigenaar expliciet maken | Deels | V2 spreekt zichzelf tegen: docker-runner dispatch versus app-orchestrator. |
| Transactionele `BootstrapRun` + `ClaudeJob` status-sync | Ja | Goed. Hou notify na commit, niet in de DB-transaction zelf. |
| `ClaudeJobKind` exhaustive consumers | Ja | Goed opgenomen. |
| `BootstrapRun.claude_job_id @unique` + reverse relation | Ja | Goed. |
| Concurrency guard | Ja | Goed, vooral met DB-level partial unique index. |
| PAT secret-boundary | Deels | Docker krijgt geen DB/secrets meer, maar PAT wordt nu in memory doorgegeven aan een background promise. Dat is niet duurzaam. |
| Action-param validatie/path-safety | Ja | Goed, maar `condition: String?` blijft een risico. |
| Zes ADR-stubs in MVP | Ja | Goed. |
| App Router paden | Ja | Goed. |
| Niet-bestaand `npm run worker` | Ja | Gecorrigeerd. |
| `Product.repo_slug` | Ja | Goed begin, maar uniekheid moet eigenlijk per GitHub owner + slug, niet per Scrum4Me user. |

## Nieuwe bevindingen

### P1 - V2 heeft nog twee executor-architecturen tegelijk

Regels 49-66 beschrijven dispatch in `scrum4me-docker/bin/run-one-job.ts`, inclusief deterministic dispatch en ephemeral PAT op job-claim. Regels 200-212 kiezen daarna voor "App is executor" en laten docker `BOOTSTRAP_REPO` juist niet claimen. Regels 285-322 werken vervolgens app-side fire-and-forget uit.

Dat is geen detail; dit bepaalt wie claimt, wie secrets heeft, wie retries doet en wie eigenaar is van leases/timeouts. Maak de keuze expliciet:

- Optie A: `bootstrap-service` claimt alleen `BOOTSTRAP_REPO`, heeft `DATABASE_URL` + `BOOTSTRAP_ENCRYPTION_KEY`, decrypt zelf de PAT per run, en gebruikt dezelfde status-sync.
- Optie B: bestaande docker-runner claimt ook deterministic jobs, maar dan moet de secret-boundary worden aangepast en gedocumenteerd.
- Optie C: Next.js app voert inline uit, maar dan geen queue/claim-semantiek en geen 60 minuten timeout claimen.

Voor Scrum4Me past Optie A het best: klein apart Node-proces, geen Claude quota, wel durable retries.

### P1 - Fire-and-forget in de app is niet betrouwbaar genoeg

Het plan kiest `runBootstrapInBackground(runId, pat)` na de server-action response. Vercel documenteert dat niet-geawait async werk in Functions kan blijven hangen in een bevroren execution context; helpers als `waitUntil()` zijn bovendien nog steeds gebonden aan de maximale function timeout. Vercel Functions hebben harde duration-limieten; het plan noemt zelf een 60-minuten watchdog, wat niet past bij normale serverless limits.

Fix: vervang `fire-and-forget` door een echte worker:

- `startBootstrapAction` maakt alleen `BootstrapRun` + `ClaudeJob`.
- `bootstrap-service` claimt atomair `BOOTSTRAP_REPO` runs.
- Service decrypt de PAT op basis van `run.user_id`, voert de recipe uit, en sync't terminal status.
- UI blijft exact hetzelfde via SSE.

### P1 - PAT doorgeven aan een background promise is de verkeerde secret-shape

Regel 197 zegt dat `startBootstrapAction` decrypt voor enqueue, en regel 298 geeft `pat` door aan de background runner. Als het proces wegvalt, is de job niet hervatbaar zonder opnieuw vanuit user context te starten. Als logs of closures uitlekken, zit de PAT in app-memory buiten een duidelijk lifecycle-contract.

Fix: geef alleen `runId` door. De executor haalt `User.github_pat_encrypted` zelf op, decrypt binnen de execution boundary, zeroized daarna best-effort, en logt nooit token-materiaal. Voeg `github_pat_verified_at`, `github_pat_scopes` en `github_pat_expires_at` toe of overweeg later GitHub App/OAuth.

### P1 - Gebruik geen lange-running local git push in een serverless function

De v2-flow gebruikt `mkdtemp`, template clone, lokale git commit, repo create en push. Dat is prima voor een worker/service, maar kwetsbaar in serverless: tijdslimieten, file descriptor limieten, cleanup bij timeout, en onduidelijke rollback wanneer push half lukt.

Fix: zet dit in `bootstrap-service` of Vercel Sandbox/Workflow. Als je toch app-side wilt blijven, maak de eerste versie veel kleiner: GitHub template endpoint aanroepen, geen lokale mutaties, geen push, geen `RUN_BASH_TEMPLATE`.

### P2 - Voeg een dry-run/preview toe voor de wizard en admin-catalog

Backstage Scaffolder heeft dry-run support en een Template Editor waarmee templates in een echte omgeving getest kunnen worden zonder externe mutaties. Scrum4Me mist dit nog.

Aanbevolen toevoeging:

- `previewBootstrapAction(productId, selections)` bouwt `recipe_snapshot`, valideert acties, draait alle non-mutating file handlers in tmpdir, en retourneert file tree + action log + warnings.
- UI toont "Review" voor "Create repo".
- Admin-UI mag een recipe pas activeren nadat dry-run groen is.
- Tests draaien per action ook in dry-run mode.

Dit verlaagt het risico van DB-gedreven recipes sterk.

### P2 - Maak repository owner/slug een echte picker, geen impliciete username

Backstage gebruikt een repository picker met allowed hosts, owners en repos. Het plan heeft `repo_slug`, maar owner blijft impliciet `user.github_username` en staat zelfs nog als open punt.

Fix voor MVP:

- `Product.repo_owner` of `BootstrapRun.repo_owner_snapshot`.
- `repo_slug` uniqueness op `(repo_owner, repo_slug)`, niet op `(user_id, repo_slug)`.
- `saveGitHubPatAction` haalt beschikbare orgs op en bewaart geen owner zonder permissiecheck.
- Wizard laat owner + slug zien en doet preflight `GET /repos/{owner}/{repo}` of equivalente Octokit call.

### P2 - Gebruik GitHub template API bewust, of leg uit waarom niet

GitHub heeft een officieel endpoint om een repository uit een template te maken. Dat is eenvoudiger en veiliger dan zelf init/remote/push doen, maar het endpoint werkt met de template repo en repo-name/owner, niet met een willekeurige tag/ref zoals `template_version`.

Aanbevolen beslissing:

- Als `template_version` hard nodig is: blijf bij "download/clone tagged template, mutate, push", maar documenteer dat GitHub's template endpoint bewust niet gebruikt wordt.
- Als default-branch voldoende is: gebruik GitHub's template endpoint voor MVP en beperk v1 tot variabelen die later via follow-up commits kunnen.

Voor dit plan zou ik tag-pinning behouden, maar de trade-off expliciet maken.

### P2 - Voeg action-permissions toe, niet alleen admin CRUD

Backstage kan parameters, steps en actions autoriseren. Scrum4Me v2 heeft alleen "admin-UI fase 2" en path-safety. Dat beschermt niet tegen een legitieme recipe die te veel doet.

Voeg toe aan `BootstrapAction` of `BootstrapOption`:

- `risk_level: LOW | MEDIUM | HIGH`
- `requires_role: ADMIN | PRODUCT_OWNER`
- `enabled: boolean`
- `supports_dry_run: boolean`
- `side_effects: FILESYSTEM | GITHUB_REPO | GITHUB_SETTINGS | NETWORK`

`RUN_BASH_TEMPLATE` en GitHub-mutaties mogen standaard alleen admin-authored en dry-run getest zijn.

### P2 - Vervang `condition: String?` door een getypte mini-DSL of haal hem uit MVP

Een vrije condition string in DB is op termijn een tweede interpreter. Gebruik liever:

```ts
condition: {
  allOf?: Array<{ category: string; option: string }>
  anyOf?: Array<{ category: string; option: string }>
  not?: Array<{ category: string; option: string }>
}
```

Valideer met Zod en snapshot de resolved action list. Voor MVP: geen conditions, alleen expliciete selected options.

### P2 - Maak template/catalog versioning scherper

Het plan heeft `template_version` en `recipe_snapshot`, maar mist nog:

- `template_source_sha` of release asset checksum.
- `catalog_version` of `recipe_hash`.
- `action_schema_version`.
- `generated_from` metadata in de nieuwe repo, bijvoorbeeld `.scrum4me/bootstrap.json`.

Dat maakt update-detection en latere "rerun/update repo" veel simpeler.

## Webresearch: vergelijkbare ideeen

### GitHub template repositories

GitHub ondersteunt "create repository using a template" via REST. Belangrijk: token scopes verschillen voor public/private repos; het endpoint accepteert `owner`, `name`, `include_all_branches` en `private`. Dit bevestigt dat owner/slug en token-scope preflight first-class moeten zijn.

Bron: <https://docs.github.com/en/rest/repos/repos#create-a-repository-using-a-template>

### Backstage Software Templates / Scaffolder

Backstage is het dichtstbijzijnde patroon: skeleton code laden, variabelen templaten, en publishen naar GitHub/GitLab. Het heeft ook built-in actions voor fetch/publish, een template editor, dry-run, secrets, repository picker en permission controls.

Relevante lessen:

- Scrum4Me's `BootstrapActionKind` lijkt sterk op Backstage scaffolder actions.
- Dry-run en template editor horen vroeg in het plan, niet pas na MVP.
- Secrets moeten apart van gewone parameters blijven.
- Repository owner/host/repo hoort een picker met policy te zijn.
- Action-level permissions zijn belangrijk als recipes in DB/admin UI leven.

Bronnen:

- <https://backstage.io/docs/features/software-templates/>
- <https://backstage.io/docs/features/software-templates/builtin-actions/>
- <https://backstage.io/docs/features/software-templates/writing-templates/>
- <https://backstage.io/docs/next/features/software-templates/dry-run-testing/>
- <https://backstage.io/docs/next/features/software-templates/authorizing-scaffolder-template-details/>

### Cookiecutter, Plop, Hygen

Cookiecutter bevestigt het template-repo model met prompts/context/replay. Plop en Hygen bevestigen het action/generator model, maar zijn vooral lokaal/dev-tooling, niet server-side repo provisioning.

Lessen voor Scrum4Me:

- Houd de action-set klein en composable.
- Zorg voor replay: bewaar parameters, template versie en recipe hash.
- Maak custom actions code-owned, niet vrij definieerbaar vanuit DB.

Bronnen:

- <https://cookiecutter.readthedocs.io/en/stable/>
- <https://plopjs.com/documentation/>
- <https://hygen.ecmascript.pizza/docs/create/>

### Vercel Functions

Omdat het plan de app als executor overweegt, zijn Vercel limits relevant. Vercel Functions hebben maximale duur en background helpers zijn nog steeds aan die max duration gebonden. Dat maakt app-side fire-and-forget ongeschikt als robuuste bootstrap-queue.

Bronnen:

- <https://vercel.com/docs/functions/limitations>
- <https://vercel.com/kb/guide/troubleshooting-inconsistent-logs-in-vercel-functions>

## Aangepaste aanbeveling voor het plan

Vervang de executor-sectie door deze keuze:

1. `BOOTSTRAP_REPO` blijft een `ClaudeJobKind` alleen voor uniforme UI/SSE/status.
2. `scrum4me-docker` claimt `BOOTSTRAP_REPO` niet.
3. Nieuwe `bootstrap-service` claimt alleen `BOOTSTRAP_REPO` of `BootstrapRun(PENDING)`.
4. Service heeft `DATABASE_URL`, `DIRECT_URL`, `BOOTSTRAP_ENCRYPTION_KEY`, geen Anthropic key nodig.
5. Service decrypt PAT per run, voert recipe uit, en gebruikt dezelfde transactionele status-sync.
6. Voeg `previewBootstrapAction` dry-run toe voor wizard en admin.
7. Voeg owner picker, action permissions, catalog versioning en `.scrum4me/bootstrap.json` toe.

Met die aanpassing wordt het plan duidelijker, veiliger en veel dichter bij bewezen scaffolder-patronen.
