--- title: "Claude ↔ User Question Channel" status: active audience: [maintainer, contributor] language: nl last_updated: 2026-05-03 related: [project-structure.md](./project-structure.md) --- ## Vraag-antwoord-kanaal Claude ↔ user (M11) Persistent kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker. Wanneer Claude tijdens een implementatie vastloopt op een keuze, schrijft hij een gestructureerde vraag naar `claude_questions`. Een Postgres-trigger emit op het **bestaande** `scrum4me_changes`-kanaal (hergebruik uit M8) met `entity: 'question'`. De Scrum4Me-app heeft een aparte user-scoped SSE-route die op dit kanaal abonneert, filter't op product-toegang en de notifications-bell in de NavBar voedt. Iedere gebruiker met product-membership kan antwoorden; story-assignee krijgt visuele emphase. Claude leest het antwoord (sync via polling met `wait_seconds`, of in een latere sessie via `get_question_answer`) en gaat door. ### Sequence ```mermaid sequenceDiagram participant C as Claude (MCP) participant DB as Postgres participant SC as scrum4me_changes channel participant SSE as /api/realtime/notifications participant U as Scrum4Me UI (browser) C->>DB: INSERT claude_questions (status=open) DB->>SC: pg_notify {entity:'question', op:'I', id, ...} SC->>SSE: notification (filter: question + product-access) SSE->>U: data event → Zustand store upsert → bell badge Note over U: Gebruiker klikt bell → Sheet → Modal U->>DB: answerQuestion(questionId, answer)
Server Action: atomic updateMany WHERE status='open' DB->>SC: pg_notify {entity:'question', op:'U', status:'answered'} SC->>SSE: notification SSE->>U: data event → store remove → bell badge -1 Note over C: Optioneel: ask_user_question(wait_seconds) polt elke 2s C->>DB: SELECT status FROM claude_questions WHERE id=... DB-->>C: status='answered', answer='...' C->>C: gaat door met implementatie ``` ### Threat-model | Aanval | Mitigatie | |---|---| | **Race**: dubbele submit op zelfde vraag | Atomic `updateMany WHERE status='open'` — één caller ziet count=1, rest count=0 met disambiguatie via second findFirst | | **Demo-account misbruik** | `requireWriteAccess` op MCP-write-tools (PERMISSION_DENIED), early-return op `session.isDemo` in answerQuestion Server Action, disabled submit + tooltip in AnswerModal | | **Cross-product leak** | `productAccessFilter` op DB-query én SSE-server-side-filter (Set met user's accessible product-IDs) | | **Cron-endpoint misbruik** | `Authorization: Bearer ${CRON_SECRET}` — Vercel injecteert automatisch; faalt 401 als secret niet gezet (geen open endpoint in dev) | | **Onbeperkte vragen-groei** | `expires_at` 24 u + Vercel cron `0 4 * * *` (dagelijks; Hobby-plan-limiet) markeert `status='expired'` → uit notifications-bell | | **Gevoelige info in logs** | Logging alleen `question_id`, nooit vraag- of antwoord-tekst | ### Waarom hergebruik scrum4me_changes-kanaal In tegenstelling tot M10 (eigen `scrum4me_pairing`-kanaal) is M11 een uitbreiding van de bestaande realtime-infra. Voordelen: - Eén Postgres-NOTIFY-listener per route i.p.v. twee — minder DB-connecties - Solo-realtime + notifications kunnen onafhankelijk evolueren via de `entity`-key - Toekomstige entities (bijv. `entity: 'comment'`, `entity: 'mention'`) hoeven geen nieuw kanaal — alleen een filter-aanpassing in de route die ze wil ontvangen Risico: een nieuwe entity vergeten te filteren leidt tot lekkage. Mitigatie: expliciet `if (payload.entity === 'X') return false` in elke SSE-route die betrokken-features niet hoort te zien (zoals de solo-route die `entity:'question'` weert). Dit patroon (notification-channel via een bestaande pg_notify-stream) is herbruikbaar — zie `docs/patterns/claude-question-channel.md`. ## Worker quota-gate flow (M13) De `worker_heartbeat` MCP-tool gebruikt hetzelfde `scrum4me_changes`-kanaal met een nieuw event-type `worker_heartbeat`: ``` Worker scrum4me-mcp Postgres SSE-route NavBar | | | | | |--worker_heartbeat(15,low)-->| | | | | |--pg_notify('scrum4me_changes', {type:'worker_heartbeat',is_low:true})-->| | | | |--data:{...}---->| | | | | |--stand-by badge ``` **Pre-flight loop** (vóór elke `wait_for_job`): 1. `get_worker_settings` → `min_quota_pct` 2. `bash scripts/worker-quota-probe.sh` → `{ pct, reset_at_iso }` 3. `worker_heartbeat(last_quota_pct, last_quota_check_at, is_low = pct < min_quota_pct)` 4. Als `is_low`: log "wachten tot quota-reset om HH:MM", slaap tot `reset_at_iso + 5s`, herhaal stap 2 5. Anders: `wait_for_job` aanroepen De solo-store houdt `lowQuotaTokenIds: Set` bij. De NavBar toont een stand-by badge wanneer `lowQuotaTokenIds.size >= connectedWorkers && connectedWorkers > 0`. ---