Brengt de docs gelijk met de werkelijkheid na PBI-46/47/50/58/59/61/63 en M12. Belangrijkste fixes: - data-model.md herschreven naar prisma/schema.prisma: nieuwe entiteiten (Idea, IdeaLog, IdeaProduct, UserQuestion, ClaudeQuestion, ClaudeJob, SprintRun, SprintTaskExecution, ClaudeWorker, LoginPairing, PushSubscription, ModelPrice, ProductMember), nieuwe enums (FAILED/EXCLUDED, OPEN/CLOSED/ARCHIVED, ADMIN, etc.) en codes (PBI/ST/T/SP-N) toegevoegd; verwijderde todos-tabel verwijderd. - glossary.md: Sprint zonder "max 1 actief" (PBI-63), Story/Task incl. FAILED/EXCLUDED, Todo verwijderd, Idea/SprintRun/ClaudeJob/ verify_result toegevoegd. - project-structure.md: app/(app)/todos vervangen door ideas/insights/jobs/manual/admin/solo; api-tree volledig. - overview.md: "geen realtime in v1" en Docker-rationale herschreven — Postgres LISTEN/NOTIFY + SSE, claude_jobs als queue, opt-in Docker-deploy-flow. - functional.md: F-08 Todo-lijst -> Ideeen-laag, F-09 multi-sprint, F-10 Task-status incl. FAILED/EXCLUDED, F-11 endpoint-lijst, navigatiestructuur, datamodel-schets en Flow 3 bijgewerkt. - README.md API-tabel: /api/todos weg, ideas/jobs/users/profile/health toegevoegd, kort over realtime/auth-pair/internal/cron. - patterns + mcp-integration runbook: Todo-/ACTIVE-references vervangen door Idea/OPEN; create_todo MCP-tool note over verwijdering. Linkcheck groen (105 files), INDEX hergegenereerd (98 docs). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
519 lines
23 KiB
Markdown
519 lines
23 KiB
Markdown
---
|
||
title: "Data Model & Prisma Schema"
|
||
status: active
|
||
audience: [maintainer, contributor]
|
||
language: nl
|
||
last_updated: 2026-05-08
|
||
related: [auth-and-sessions.md](./auth-and-sessions.md)
|
||
---
|
||
|
||
## Datamodel
|
||
|
||
> Bron van waarheid is [`prisma/schema.prisma`](../../prisma/schema.prisma); dit document samenvat de tabellen en sleutelinvarianten. Bij twijfel wint het schema.
|
||
|
||
### `users`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| username | String | unique, not null, min 3 | Inlognaam |
|
||
| email | String? | unique | Optioneel; gebruikt voor wachtwoord-reset-flows |
|
||
| password_hash | String | not null | bcrypt hash (cost factor 12) |
|
||
| is_demo | Boolean | default false | Demo-gebruiker heeft read-only rechten |
|
||
| bio | String? | max 160 | Korte profielomschrijving |
|
||
| bio_detail | String? | max 2000 | Uitgebreide profielbeschrijving |
|
||
| must_reset_password | Boolean | default false | Forceert wachtwoord-reset bij volgende login |
|
||
| avatar_data | Bytes? | | Profielfoto als WebP bytea (max 700×700) |
|
||
| active_product_id | String? | FK → products (SetNull) | Persistente actieve PB-keuze (M9) |
|
||
| idea_code_counter | Int | default 0 | Sequentiële teller voor user-scoped idea-codes |
|
||
| min_quota_pct | Int | default 20 | Worker stand-by-drempel (M13 quota-check) |
|
||
| created_at | DateTime | default now() | |
|
||
| updated_at | DateTime | auto-update | Cache-buster voor avatar-URL |
|
||
|
||
**Indexes:** `username` (unique), `email` (unique), `active_product_id`
|
||
|
||
---
|
||
|
||
### `user_roles`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| role | Enum | `PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER \| ADMIN` | |
|
||
|
||
**Constraint:** unique `(user_id, role)`
|
||
|
||
---
|
||
|
||
### `api_tokens`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| token_hash | String | unique, not null | SHA-256 hash van het token |
|
||
| label | String? | | Bijv. "Claude Code — laptop" |
|
||
| created_at | DateTime | default now() | |
|
||
| revoked_at | DateTime? | | Null = actief |
|
||
|
||
**Indexes:** `token_hash` (unique). Eén token kan max. één `claude_workers`-record hebben.
|
||
|
||
---
|
||
|
||
### `products`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | Eigenaar |
|
||
| name | String | not null | Uniek per gebruiker |
|
||
| code | String? | max 30 | Optionele afkorting; uniek per gebruiker als gezet |
|
||
| description | String? | | |
|
||
| repo_url | String? | | Gevalideerde URL |
|
||
| definition_of_done | String | not null | Vaste instelling per product |
|
||
| auto_pr | Boolean | default false | Automatische PR creëren na sprint-completion |
|
||
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH`, default `SPRINT` | Granulariteit voor auto-PR |
|
||
| archived | Boolean | default false | |
|
||
| created_at | DateTime | default now() | |
|
||
| updated_at | DateTime | auto-update | |
|
||
|
||
**Indexes:** `(user_id, archived)` — standaard query filtert op actieve producten
|
||
**Constraints:** unique `(user_id, name)`, unique `(user_id, code)`
|
||
|
||
---
|
||
|
||
### `pbis`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| product_id | String | FK → products (Cascade) | |
|
||
| code | String | max 30, not null | Verplicht; auto-gegenereerd of handmatig |
|
||
| title | String | not null | |
|
||
| description | String? | | |
|
||
| priority | Int | 1–4 | 1 = Kritiek, 4 = Laag |
|
||
| sort_order | Float | not null | Float voor volgorde tussen items zonder renummering |
|
||
| status | Enum | `READY \| BLOCKED \| FAILED \| DONE`, default `READY` | Auto-promotie naar DONE bij sprint-close |
|
||
| pr_url | String? | | URL van de PR die deze PBI dekt (PBI-strategie) |
|
||
| pr_merged_at | DateTime? | | Gezet wanneer de PR daadwerkelijk gemerged is |
|
||
| created_at | DateTime | default now() | |
|
||
| updated_at | DateTime | auto-update | |
|
||
|
||
**Indexes:** `(product_id, priority, sort_order)`, `(product_id, status)`
|
||
**Constraint:** unique `(product_id, code)`
|
||
|
||
**Cascade-regel (sprint-close):** wanneer een Sprint wordt afgerond via `completeSprintAction` en alle stories van een PBI eindigen op DONE, zet diezelfde transactie de PBI-status op DONE. Promotie alléén — een DONE-PBI wordt nooit automatisch teruggezet. Stories die niet in deze Sprint zaten worden meegerekend op hun huidige DB-status. Een PBI zonder stories blijft READY.
|
||
|
||
---
|
||
|
||
### `stories`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| pbi_id | String | FK → pbis (Cascade) | |
|
||
| product_id | String | FK → products | Denormalisatie voor snellere queries |
|
||
| sprint_id | String? | FK → sprints | Null = in Product Backlog |
|
||
| assignee_id | String? | FK → users (SetNull) | Story-claim op het Solo-bord |
|
||
| code | String | max 30, not null | Auto-gegenereerd of handmatig |
|
||
| title | String | not null | |
|
||
| description | String? | | |
|
||
| acceptance_criteria | String? | | |
|
||
| priority | Int | 1–4 | |
|
||
| sort_order | Float | not null | |
|
||
| status | Enum | `OPEN \| IN_SPRINT \| DONE \| FAILED`, default `OPEN` | |
|
||
| created_at | DateTime | default now() | |
|
||
| updated_at | DateTime | auto-update | |
|
||
|
||
**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`, `(sprint_id, assignee_id)`
|
||
**Constraint:** unique `(product_id, code)`
|
||
|
||
**Auto-promotie/demotie via task-status:** zodra alle tasks van een story op `DONE` staan en de story-status nog niet `DONE` is, promoot dezelfde transactie de story naar `DONE`. Wordt een task van een `DONE`-story heropend, dan demoot de story terug naar `IN_SPRINT` — niet naar `OPEN` (dat zou "terug in productbacklog" betekenen, een sprint-management-actie). Logica zit in [lib/tasks-status-update.ts](../../lib/tasks-status-update.ts) en wordt aangeroepen door alle drie de task-status-write-paden (`updateTaskStatusAction`, `saveTask` edit-mode, REST `PATCH /api/tasks/[id]`).
|
||
|
||
---
|
||
|
||
### `story_logs`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| story_id | String | FK → stories (Cascade) | |
|
||
| type | Enum | `IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT` | |
|
||
| content | String | not null | |
|
||
| status | Enum | `PASSED \| FAILED`? | Alleen bij type `TEST_RESULT` |
|
||
| commit_hash | String? | | Alleen bij type `COMMIT` |
|
||
| commit_message | String? | | Alleen bij type `COMMIT` |
|
||
| metadata | Json? | | Vrije bag voor bron-info (bv. agent, model_id) |
|
||
| created_at | DateTime | default now() | |
|
||
|
||
**Indexes:** `(story_id, created_at)`
|
||
|
||
---
|
||
|
||
### `sprints`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| product_id | String | FK → products (Cascade) | |
|
||
| code | String | max 30, not null | Auto-gegenereerd `SP-N` per product (PBI-59) |
|
||
| sprint_goal | String | not null | |
|
||
| status | Enum | `OPEN \| CLOSED \| ARCHIVED \| FAILED`, default `OPEN` | |
|
||
| start_date | Date? | | Optionele planningsmetadata |
|
||
| end_date | Date? | | Optionele planningsmetadata |
|
||
| created_at | DateTime | default now() | |
|
||
| completed_at | DateTime? | | Wordt gezet bij overgang naar CLOSED |
|
||
|
||
**Indexes:** `(product_id, status)`
|
||
**Constraint:** unique `(product_id, code)`
|
||
|
||
**Eén product, meerdere sprints (PBI-63):** een product kan tegelijk meer dan één sprint hebben. `OPEN` is geen exclusieve status; de sprint-switcher in de product-header laat de gebruiker tussen sprints kiezen. Stories in `IN_SPRINT` linken via `sprint_id` naar één specifieke sprint.
|
||
|
||
---
|
||
|
||
### `sprint_runs`
|
||
|
||
Eén `sprint_runs`-record per uitvoering van de SPRINT_IMPLEMENTATION-flow (PBI-46/47/50). Houdt status, branch en chained retries bij wanneer een run is gepauzeerd of mislukt.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| sprint_id | String | FK → sprints (Cascade) | |
|
||
| started_by_id | String | FK → users | Wie de run startte |
|
||
| status | Enum | `QUEUED \| RUNNING \| PAUSED \| DONE \| FAILED \| CANCELLED` | |
|
||
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH` | Snapshot van de strategie bij start |
|
||
| branch | String? | | Werkbranch voor de run |
|
||
| pr_url | String? | | |
|
||
| started_at / finished_at | DateTime? | | |
|
||
| failure_reason | String? | | Vrij tekstveld bij FAILED/CANCELLED |
|
||
| failed_task_id | String? | FK → tasks (SetNull) | Eerste task die de run brak (cascade-FAIL) |
|
||
| pause_context | Json? | | Gevalideerd door Zod (`lib/sprint-run/pause-context.ts`) |
|
||
| previous_run_id | String? | unique, FK → sprint_runs (SetNull) | Chain naar een eerdere run |
|
||
| created_at / updated_at | DateTime | | |
|
||
|
||
**Indexes:** `(sprint_id, status)`, `(started_by_id, status)`
|
||
|
||
---
|
||
|
||
### `tasks`
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| story_id | String | FK → stories (Cascade) | |
|
||
| product_id | String | FK → products (Cascade) | Denormalisatie voor product-scoped queries |
|
||
| sprint_id | String? | FK → sprints | Denormalisatie; geërfd van story bij sprint-toevoeging |
|
||
| code | String | max 30, not null | Auto-gegenereerd `T-N` per product |
|
||
| title | String | not null | |
|
||
| description | String? | | |
|
||
| implementation_plan | String? | | Opgeslagen via Server Action of `PATCH /api/tasks/:id` |
|
||
| priority | Int | 1–4 | |
|
||
| sort_order | Float | not null | |
|
||
| status | Enum | `TO_DO \| IN_PROGRESS \| REVIEW \| DONE \| FAILED \| EXCLUDED`, default `TO_DO` | `EXCLUDED` slaat verify-skip op tijdens een sprint-run |
|
||
| verify_only | Boolean | default false | Run-mode: alleen verifiëren, niet implementeren |
|
||
| verify_required | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY`, default `ALIGNED_OR_PARTIAL` | Drempel waarop een job's verify_result als acceptabel telt |
|
||
| repo_url | String? | | Optionele override van `product.repo_url` voor tasks die in een andere repo wonen |
|
||
| created_at | DateTime | default now() | |
|
||
| updated_at | DateTime | auto-update | |
|
||
|
||
**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`, `(product_id)`
|
||
**Constraint:** unique `(product_id, code)` — `code` blijft stabiel bij re-parenting (Jira-stijl)
|
||
|
||
---
|
||
|
||
### `claude_jobs`
|
||
|
||
Job-queue waarop `wait_for_job` (MCP) atomisch claimt via `FOR UPDATE SKIP LOCKED`. Eén rij per task-implementatie, idea-grill, idea-make-plan, plan-chat of sprint-run.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| product_id | String | FK → products (Cascade) | |
|
||
| task_id | String? | FK → tasks (Cascade) | Bij `TASK_IMPLEMENTATION` of `SPRINT_IMPLEMENTATION` |
|
||
| idea_id | String? | FK → ideas (Cascade) | Bij `IDEA_GRILL` / `IDEA_MAKE_PLAN` |
|
||
| sprint_run_id | String? | FK → sprint_runs (SetNull) | Koppel naar de bovenliggende run |
|
||
| kind | Enum | `TASK_IMPLEMENTATION \| IDEA_GRILL \| IDEA_MAKE_PLAN \| PLAN_CHAT \| SPRINT_IMPLEMENTATION` | |
|
||
| status | Enum | `QUEUED \| CLAIMED \| RUNNING \| DONE \| FAILED \| CANCELLED \| SKIPPED` | |
|
||
| claimed_by_token_id | String? | FK → api_tokens (SetNull) | Auth-koppel voor `update_job_status` |
|
||
| claimed_at / started_at / finished_at / pushed_at | DateTime? | | Lifecycle-stempels |
|
||
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | Alleen voor task-/sprint-jobs |
|
||
| model_id | String? | | Anthropic model dat de agent rapporteerde |
|
||
| input_tokens / output_tokens / cache_read_tokens / cache_write_tokens | Int? | | Token-usage voor billing-overzicht |
|
||
| plan_snapshot | String? | | Bevroren plan op claim-moment |
|
||
| base_sha / head_sha / branch / pr_url | String? | | Git-context |
|
||
| summary | String? | | Vrije agent-samenvatting bij DONE |
|
||
| error | String? | | Reden bij FAILED |
|
||
| retry_count | Int | default 0 | |
|
||
| lease_until | DateTime? | | Stale-CLAIMED → terug naar QUEUED na 30 min |
|
||
| created_at / updated_at | DateTime | | |
|
||
|
||
**Indexes:** `(user_id, status)`, `(task_id, status)`, `(idea_id, status)`, `(sprint_run_id, status)`, `(status, claimed_at)`, `(status, finished_at)`, `(status, lease_until)`
|
||
|
||
---
|
||
|
||
### `sprint_task_executions`
|
||
|
||
Bevroren scope-snapshot per `SPRINT_IMPLEMENTATION`-claim (PBI-50). Bij claim wordt voor elke `TO_DO`-task in scope één `PENDING`-record gemaakt met `implementation_plan` + `verify_required` gesnapshot. Worker en gate werken uitsluitend op deze rows; latere wijzigingen aan Task hebben geen invloed op de lopende batch.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| sprint_job_id | String | FK → claude_jobs (Cascade) | De parent SPRINT_IMPLEMENTATION-job |
|
||
| task_id | String | FK → tasks (Cascade) | |
|
||
| order | Int | not null | Volgorde binnen de batch |
|
||
| plan_snapshot | String (Text) | not null | Het bevroren implementation_plan |
|
||
| verify_required_snapshot | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY` | |
|
||
| verify_only_snapshot | Boolean | default false | |
|
||
| base_sha / head_sha | String? | | |
|
||
| status | Enum | `PENDING \| RUNNING \| DONE \| FAILED \| SKIPPED` | |
|
||
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | |
|
||
| verify_summary / skip_reason | String? (Text) | | |
|
||
| started_at / finished_at | DateTime? | | |
|
||
| created_at / updated_at | DateTime | | |
|
||
|
||
**Constraint:** unique `(sprint_job_id, task_id)`
|
||
**Indexes:** `(sprint_job_id, order)`
|
||
|
||
---
|
||
|
||
### `model_prices`
|
||
|
||
Prijslookup voor Anthropic-modellen, gebruikt door de jobs-pagina om kosten te berekenen.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| model_id | String | unique | Anthropic model-id (bv. `claude-opus-4-7`) |
|
||
| input_price_per_1m | Decimal(12,6) | | USD per 1M tokens (default) |
|
||
| output_price_per_1m | Decimal(12,6) | | |
|
||
| cache_read_price_per_1m | Decimal(12,6) | | |
|
||
| cache_write_price_per_1m | Decimal(12,6) | | |
|
||
| currency | String | default `USD` | |
|
||
| created_at / updated_at | DateTime | | |
|
||
|
||
---
|
||
|
||
### `claude_workers`
|
||
|
||
Live-presence-record per actieve agent worker. Ingevoegd bij MCP-startup, geüpdatet via `worker_heartbeat` (5s), opgeruimd bij SIGTERM. NavBar telt actieve workers op `last_seen_at < now() - 15s`.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| token_id | String | unique, FK → api_tokens (Cascade) | Eén worker per token |
|
||
| product_id | String? | | Optioneel; gerapporteerd door de agent |
|
||
| started_at / last_seen_at | DateTime | default now() | |
|
||
| last_quota_pct | Int? | | M13 pre-flight quota-check |
|
||
| last_quota_check_at | DateTime? | | |
|
||
|
||
**Indexes:** `(user_id, last_seen_at)`
|
||
|
||
---
|
||
|
||
### `product_members`
|
||
|
||
Koppelt Developer-gebruikers aan een product backlog. De eigenaar (`products.user_id`) heeft altijd volledige toegang; via `product_members` kunnen aanvullende Developers leesrechten en schrijfrechten op stories, taken en sprints van dat product krijgen. Rollen worden niet hier opgeslagen — dat doet `user_roles`. Een gebruiker kan alleen worden toegevoegd als hij/zij de rol `DEVELOPER` heeft.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| product_id | String | FK → products (Cascade) | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| created_at | DateTime | default now() | |
|
||
|
||
**Constraint:** unique `(product_id, user_id)`
|
||
**Indexes:** `(user_id)`
|
||
|
||
---
|
||
|
||
### `ideas`
|
||
|
||
Idea-entity (M12) tussen losse notitie en PBI. Een idea wordt eerst _gegrilled_ (interactieve Q&A → `grill_md`), daarna gemateriaaliseerd tot een plan (`plan_md`) dat deterministisch geparseerd wordt naar PBI + stories + tasks. Vervangt de oude `todos`-tabel volledig (atomische migratie ST-1239 — todos zijn gedropt).
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| product_id | String? | FK → products (SetNull) | Primaire scope; null = unscoped capture |
|
||
| code | String | max 30, not null | Sequentieel per gebruiker via `idea_code_counter` |
|
||
| title | String | not null | |
|
||
| description | String? | max 4000 | Initiële tekst |
|
||
| grill_md | String? (Text) | | Output van IDEA_GRILL |
|
||
| plan_md | String? (Text) | | Output van IDEA_MAKE_PLAN |
|
||
| pbi_id | String? | unique, FK → pbis (SetNull) | Wordt gevuld na materialisatie |
|
||
| status | Enum | `DRAFT \| GRILLING \| GRILL_FAILED \| GRILLED \| PLANNING \| PLAN_FAILED \| PLAN_READY \| PLANNED`, default `DRAFT` | |
|
||
| archived | Boolean | default false | |
|
||
| created_at / updated_at | DateTime | | |
|
||
|
||
**Constraint:** unique `(user_id, code)`
|
||
**Indexes:** `(user_id, archived, status)`, `(user_id, product_id)`
|
||
|
||
---
|
||
|
||
### `idea_products`
|
||
|
||
Optionele secundaire producten waar een idea ook impact heeft.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| idea_id | String | FK → ideas (Cascade) | |
|
||
| product_id | String | FK → products (Cascade) | |
|
||
| created_at | DateTime | default now() | |
|
||
|
||
**Constraint:** unique `(idea_id, product_id)`
|
||
**Indexes:** `(product_id)`
|
||
|
||
---
|
||
|
||
### `idea_logs`
|
||
|
||
Activiteitenlog per idea — soortgelijke rol als `story_logs` voor stories.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| idea_id | String | FK → ideas (Cascade) | |
|
||
| type | Enum | `DECISION \| NOTE \| GRILL_RESULT \| PLAN_RESULT \| STATUS_CHANGE \| JOB_EVENT` | |
|
||
| content | String (Text) | not null | |
|
||
| metadata | Json? | | |
|
||
| created_at | DateTime | default now() | |
|
||
|
||
**Indexes:** `(idea_id, created_at)`
|
||
|
||
---
|
||
|
||
### `user_questions`
|
||
|
||
Vrije vragen die een gebruiker aan zichzelf stelt op een idea (M12). Wordt door de Idea-detail-UI ingelezen om te helpen bij het grillen.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| idea_id | String | FK → ideas (Cascade) | |
|
||
| user_id | String | not null | Eigenaar |
|
||
| question | String (Text) | not null | |
|
||
| answer | String? (Text) | | |
|
||
| status | Enum (lowercase) | `pending \| answered`, default `pending` | |
|
||
| created_at / updated_at | DateTime | | |
|
||
|
||
**Indexes:** `(idea_id, status)`, `(user_id)`
|
||
|
||
---
|
||
|
||
### `claude_questions`
|
||
|
||
Persistent vraag-antwoord-kanaal van een agent (via `mcp__scrum4me__ask_user_question`) richting de actieve gebruiker (M11). LISTEN/NOTIFY pusht het antwoord terug naar de wachtende agent. Zie [architecture/claude-question-channel.md](./claude-question-channel.md).
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| story_id | String? | FK → stories (Cascade) | Eén van story/task/idea is verplicht |
|
||
| task_id | String? | FK → tasks (SetNull) | |
|
||
| idea_id | String? | FK → ideas (Cascade) | |
|
||
| product_id | String | FK → products (Cascade) | Gedenormaliseerd voor het SSE-filter |
|
||
| asked_by | String | FK → users | Token-houder = Claude |
|
||
| question | String (Text) | not null | |
|
||
| options | Json? | | `string[]` voor multi-choice; null voor free-text |
|
||
| status | String | not null | `open \| answered \| cancelled \| expired` |
|
||
| answer | String? (Text) | | |
|
||
| answered_by | String? | FK → users | |
|
||
| answered_at | DateTime? | | |
|
||
| created_at | DateTime | default now() | |
|
||
| expires_at | DateTime | not null | Default `now() + 24h`, ingesteld door MCP-tool |
|
||
|
||
**Indexes:** `(story_id, status)`, `(idea_id, status)`, `(product_id, status)`, `(status, expires_at)`
|
||
|
||
---
|
||
|
||
### `login_pairings`
|
||
|
||
QR-pairing-flow (M10). Desktop start een pairing, telefoon scant en bevestigt; daarna kan de desktop ruilen voor een sessie.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| secret_hash | String | not null | Hash van het pairing-secret |
|
||
| desktop_token_hash | String | not null | Hash van het pre-auth desktop-token |
|
||
| status | String | not null | `pending \| approved \| consumed \| expired` |
|
||
| user_id | String? | FK → users (SetNull) | Gezet bij approval |
|
||
| desktop_ua | String? | max 255 | UA-string van de desktop-aanvraag |
|
||
| desktop_ip | String? | max 45 | IP voor audit |
|
||
| created_at | DateTime | default now() | |
|
||
| expires_at | DateTime | not null | TTL voor afhandeling |
|
||
| approved_at / consumed_at | DateTime? | | |
|
||
|
||
**Indexes:** `(expires_at)`, `(status, expires_at)`
|
||
|
||
---
|
||
|
||
### `push_subscriptions`
|
||
|
||
Web Push subscriptions per gebruiker, voor notificaties.
|
||
|
||
| Kolom | Type | Constraints | Noten |
|
||
|---|---|---|---|
|
||
| id | String (cuid) | PK | |
|
||
| user_id | String | FK → users (Cascade) | |
|
||
| endpoint | String | unique, not null | Push-service endpoint |
|
||
| p256dh / auth | String | not null | VAPID keys |
|
||
| user_agent | String? | | UA bij subscribe |
|
||
| created_at / last_used_at | DateTime | default now() | |
|
||
|
||
**Indexes:** `(user_id)`
|
||
|
||
---
|
||
|
||
## Toegangsmodel en schrijfbeveiliging
|
||
|
||
Producttoegang is centraal gedefinieerd als:
|
||
|
||
- eigenaar: `products.user_id === gebruiker.id`
|
||
- teamlid: `product_members` bevat `(product_id, user_id)`
|
||
|
||
Code gebruikt hiervoor `productAccessFilter(userId)` uit `lib/product-access.ts`. Route Handlers en Server Actions mogen geen eigenaar-only filter (`user_id`) gebruiken voor product-scoped resources tenzij het expliciet om eigenaarsbeheer gaat (archiveren, teamleden beheren).
|
||
|
||
Schrijfoperaties volgen deze invarianten:
|
||
|
||
- Controleer eerst authenticatie en `session.isDemo`.
|
||
- Valideer input met Zod, maar behandel TypeScript types niet als runtime-beveiliging.
|
||
- Controleer de parent-resource met `productAccessFilter`.
|
||
- Vertrouw bulk-ID's nooit los: haal de records eerst op met `id in (...)` plus de parent-scope (`product_id`, `pbi_id`, `sprint_id` of `story_id`) en weiger de operatie als aantallen niet exact overeenkomen.
|
||
- Weiger dubbele IDs in reorder- en beslissingslijsten.
|
||
- Leid denormalized foreign keys af van de database-parent (`pbi.product_id`, `sprint.product_id`) en niet van form-data of JSON body.
|
||
- Delete of update alleen nadat de resource scoped is gevonden; gebruik scoped `deleteMany`/`updateMany` wanneer een unique `delete` anders onveilig zou zijn.
|
||
|
||
---
|
||
|
||
## Enums (overzicht)
|
||
|
||
| Enum | Waarden |
|
||
|---|---|
|
||
| `Role` | `PRODUCT_OWNER`, `SCRUM_MASTER`, `DEVELOPER`, `ADMIN` |
|
||
| `PbiStatus` | `READY`, `BLOCKED`, `FAILED`, `DONE` |
|
||
| `StoryStatus` | `OPEN`, `IN_SPRINT`, `DONE`, `FAILED` |
|
||
| `TaskStatus` | `TO_DO`, `IN_PROGRESS`, `REVIEW`, `DONE`, `FAILED`, `EXCLUDED` |
|
||
| `SprintStatus` | `OPEN`, `CLOSED`, `ARCHIVED`, `FAILED` |
|
||
| `SprintRunStatus` | `QUEUED`, `RUNNING`, `PAUSED`, `DONE`, `FAILED`, `CANCELLED` |
|
||
| `PrStrategy` | `SPRINT`, `STORY`, `SPRINT_BATCH` |
|
||
| `LogType` | `IMPLEMENTATION_PLAN`, `TEST_RESULT`, `COMMIT` |
|
||
| `TestStatus` | `PASSED`, `FAILED` |
|
||
| `ClaudeJobStatus` | `QUEUED`, `CLAIMED`, `RUNNING`, `DONE`, `FAILED`, `CANCELLED`, `SKIPPED` |
|
||
| `ClaudeJobKind` | `TASK_IMPLEMENTATION`, `IDEA_GRILL`, `IDEA_MAKE_PLAN`, `PLAN_CHAT`, `SPRINT_IMPLEMENTATION` |
|
||
| `VerifyResult` | `ALIGNED`, `PARTIAL`, `EMPTY`, `DIVERGENT` |
|
||
| `VerifyRequired` | `ALIGNED`, `ALIGNED_OR_PARTIAL`, `ANY` |
|
||
| `SprintTaskExecutionStatus` | `PENDING`, `RUNNING`, `DONE`, `FAILED`, `SKIPPED` |
|
||
| `IdeaStatus` | `DRAFT`, `GRILLING`, `GRILL_FAILED`, `GRILLED`, `PLANNING`, `PLAN_FAILED`, `PLAN_READY`, `PLANNED` |
|
||
| `IdeaLogType` | `DECISION`, `NOTE`, `GRILL_RESULT`, `PLAN_RESULT`, `STATUS_CHANGE`, `JOB_EVENT` |
|
||
| `UserQuestionStatus` | `pending`, `answered` (lowercase, niet UPPER_SNAKE) |
|
||
|
||
> API-grens: `TaskStatus` en `StoryStatus` worden tussen DB (UPPER_SNAKE) en API (lowercase) vertaald via `lib/task-status.ts` (zie [ADR-0004](../adr/0004-status-enum-mapping.md)).
|
||
|
||
---
|
||
|
||
## Prisma Schema
|
||
|
||
De volledige, levende definitie staat in [`prisma/schema.prisma`](../../prisma/schema.prisma). Genereer de ERD lokaal met `npm run db:erd` (zie [README — Database](../../README.md#database)). Het ERD-diagram zelf staat in [docs/assets/erd.svg](../assets/erd.svg).
|