Scrum4Me/docs/architecture/data-model.md
Janpeter Visser f7464db837
docs: sync data-model, glossary en specs met huidig schema (#164)
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>
2026-05-08 08:16:44 +02:00

23 KiB
Raw Blame History


Datamodel

Bron van waarheid is 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 14 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 14
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 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 14
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.

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).


Prisma Schema

De volledige, levende definitie staat in prisma/schema.prisma. Genereer de ERD lokaal met npm run db:erd (zie README — Database). Het ERD-diagram zelf staat in docs/assets/erd.svg.