Repo-indeling & tech stack
Deze pagina beschrijft de doelarchitectuur op het niveau van repo-indeling, solution-/projectstructuur en techniekkeuze. Backend én frontend worden vanaf 0 opgebouwd als een standalone systeem; de implementatie in app/ conformeert zich aan het domeinmodel en de termen. Het bestaande prototype (in bron/) is referentie/uitgangspunt, geen basis. Versienummers worden bewust pas bij de implementatie vastgepind (cloud- en versie-agnostisch); de keuzes hieronder zijn de richting.
Repo-indeling
Section titled “Repo-indeling”Alles staat op één branch (main) van één repository, verdeeld over drie mappen met elk een eigen rol. Documentatie en code leven bewust naast elkaar, zodat een modelwijziging en de bijbehorende code-aanpassing in dezelfde commit-historie zichtbaar zijn.
| Map | Rol | Status |
|---|---|---|
docs/ | Astro Starlight-documentatiesite: domeinmodel, functionele beschrijving, architectuur, scenario’s, termen, changelog. Source of truth voor het model. | Bestaand |
app/ | De implementatie: een .NET-solution (backend) plus een frontend-monorepo, vanaf 0 opgebouwd tegen docs/ als spec. | Nieuw op te bouwen |
bron/ | Bronmateriaal: functioneel ontwerp (PDF + mockups) en het prototype als read-only referentie/uitgangspunt — niet de basis van app/. | Bestaand |
iseeyou/ # repo-root, branch: main├─ docs/ # Astro Starlight (source of truth)│ └─ src/content/docs/iseeyou/...├─ app/ # implementatie vanaf 0 (npm-workspaces-monorepo + .NET-solution)│ ├─ backend/ # .NET-solution (modulaire monoliet, DDD)│ │ └─ ISeeYou.slnx│ ├─ frontend/ # gebruikersportaal (React 19 + Vite + TypeScript)│ ├─ beheer/ # beheerportaal (React 19 + Vite + TypeScript)│ └─ shared/ # @iseeyou/shared: Toegangspoort, api-basis, design-tokens, types, helpers└─ bron/ # functioneel ontwerp + prototype (referentie) ├─ functioneel-ontwerp/ └─ prototype/Werkwijze (docs-first)
Section titled “Werkwijze (docs-first)”Omdat docs en code op dezelfde branch staan, is er geen cross-repo issuebrug nodig. In plaats daarvan geldt bij elke modelwijziging een lokale docs-first-discipline:
- Werk eerst
docs/bij — het domeinmodel, de termen en het/de geraakte scenario(‘s). - Bump de modelversie en voeg een changelog-entry toe.
- Bouw en controleer (
cd docs && npm run build): diagrammen renderen, geen kapotte links. - Laat daarna de code in
app/het model volgen.
Een modelfout die tijdens de implementatie wordt ontdekt, wordt eerst in
docs/gecorrigeerd en daarna in de code gevolgd — niet stilletjes in code afwijken. Zie Beslissingen (ADR’s).
Backend: .NET-solution
Section titled “Backend: .NET-solution”De backend is een modulaire monoliet in .NET / C#, gelaagd en volgens Domain-Driven Design. Eén deployable, met één module per bounded context (Beoordeling, Gebruiker & Toegang, Moderatie & Retentie, Beheer). De solution is stateless ontworpen, zodat horizontaal opschalen mogelijk blijft.
Solution-/projectstructuur
Section titled “Solution-/projectstructuur”| Project | Laag | Verantwoordelijkheid |
|---|---|---|
ISeeYou.Domain | Domein (puur) | Aggregates, entities, value objects en domeinregels per bounded context. Geen frameworks, geen I/O. |
ISeeYou.Application | Applicatie | Use-cases als dunne, expliciete handlers (geen MediatR). Definieert de poorten: IRepository<T>, IMediaStore, IRealtimeNotifier, IClock, IPasswordHasher, IAiPersonaService, IModeratieDienst, IUitslagNotificaties. |
ISeeYou.Infrastructure | Infrastructuur | Implementaties: EF Core/Npgsql-repositories, media-opslag, hashing, de vision-gebaseerde IAiPersonaService (OpenAI gpt-4o-mini + FFmpeg voor video-frames, met deterministische terugval), de OpenAiModeratieDienst (tekstmoderatie, fail-open), de uitslagnotificatie op Azure Communication Services Email (fail-graceful) en BackgroundServices (o.a. retentie). |
ISeeYou.Api | API / compositieroot | Minimal API-endpoints, SignalR-hubs en de DI-compositieroot die alles bekabelt. |
test/* | Tests | xUnit voor domein/applicatie; Testcontainers-Postgres voor integratie/endpoints/mapping; SignalR-testclient. |
Afhankelijkheidsregels
Section titled “Afhankelijkheidsregels”Afhankelijkheden wijzen strikt naar binnen. Het domein weet niets van de buitenwereld; infrastructuur en API hangen af van de binnenliggende lagen, nooit andersom.
flowchart LR
Api["ISeeYou.Api (Minimal API + SignalR, compositieroot)"]
Infra["ISeeYou.Infrastructure (EF Core/Npgsql, media, hashing, AI-persona, BackgroundServices)"]
App["ISeeYou.Application (use-cases + poorten)"]
Domain["ISeeYou.Domain (puur)"]
Api --> Infra
Infra --> App
App --> Domain
Dependency inversion.
ISeeYou.Applicationdefinieert poorten (IRepository<T>,IMediaStore,IRealtimeNotifier,IClock,IPasswordHasher,IAiPersonaService,IModeratieDienst,IUitslagNotificaties);ISeeYou.Infrastructureimplementeert ze;ISeeYou.Apikoppelt ze via DI. Zo blijft de applicatielaag onafhankelijk van EF Core, de opslag-backend, de realtime-techniek, de concrete invulling van de AI-persona (OpenAI-vision + FFmpeg), de tekstmoderatie en de uitslagnotificatie (Azure Communication Services Email).
Twee portalen, één API
Section titled “Twee portalen, één API”Er zijn twee frontends (beheerportaal, gebruikersportaal) op één deployable API. De scheiding loopt via autorisatiebeleid (IsBeheerder vs. gebruiker/anoniem), niet via een 1-op-1 context-grens. Het systeem is single-tenant (geen multi-tenant).
| Portaal | Dekt grofweg |
|---|---|
| Beheerportaal | Context Beheer + de admin-kant van Moderatie & Retentie |
| Gebruikersportaal | Beoordeling (incl. credits en waarderen) + melden + de gebruikerskant van Gebruiker & Toegang |
Authenticatie & autorisatie
Section titled “Authenticatie & autorisatie”Geen externe identity-provider; auth zit in-app en is afgestemd op de anonimiteitsbelofte.
| Doelgroep | Mechanisme |
|---|---|
| Beheerders | ASP.NET Core Identity (in-app, gehashte wachtwoorden, MFA mogelijk, rol Beheerder). |
| Gebruikers | Custom anonieme auth via een eigen Gebruiker/Sessie-aggregate, Argon2id-hashing en een fictieve naam (behoudt anonimiteit). Een geregistreerde gebruiker kan optioneel een e-mailadres opgeven, uitsluitend voor wachtwoordherstel; zonder e-mail is er geen herstel, en anonieme gebruikers hebben sowieso geen herstel. Single-device cookie-sessie. |
Autorisatie verloopt via policies. Zie Beveiliging & privacy en Backend & API.
Persistentie & realtime in het kort
Section titled “Persistentie & realtime in het kort”- EF Core + PostgreSQL (Npgsql). Value objects als owned types;
Scores(op basis van de beheerbare secties/attributen) alsjsonben de optionele reactie als nullable tekstkolom; enums via value conversion naar string;Score1..5(per2026-07-05; zo gebouwd) enWaardering0..5met check-constraint. De reviewer-waarderingsstatistieken (aantalBeoordelingen,gemiddeldeWaardering) staan als tellers op de Gebruiker; hetcredits-saldo (>= 0) idem. Optimistic concurrency viaxmin. Migrations viadotnet ef; seed van de Keuzelijst-stamdata (gender/achtergrond, secties/attributen en een vlakke regio-lijst). - Vluchtig vs. persistent in dezelfde database: vluchtig (Vraag/Opinie/Opdracht/AnoniemeGebruiker/Sessie) met een timestamp-index en periodiek opgeschoond; persistent (Gebruiker/Bericht/Keuzelijst/Systeem).
- Realtime via SignalR. Gebouwd is één
BeoordelingHub(/hubs/beoordeling) met de server-eventsOpdrachtBeschikbaar(nieuw werk in de pool) enVraagBeoordeeld(uitslag klaar); REST voor alle state-mutaties, SignalR alleen voor push. De frontend pollt nu nog (de SignalR-client is een follow-up); een aparteAanwezigheidHubvoor presence is doelbeeld, nog niet gebouwd. Bij meerdere instances een Redis-backplane (config, geen herontwerp). Zie Backend & API — Realtime. - Media in object-/blob-storage via de
IMediaStore-abstractie (de database bewaart alleen key + metadata). Retentie via een altijd-actieveRetentieHostedService : BackgroundService. Zie Backend & API — Persistentie en Media & opname. - Geen landveld, geen locatie-tracking. (Per
2026-07-11m, ADR-037) is er geenLand-veld meer: I See You wordt per land als aparte site gehost (NL eerst). Er is dus geen GeoIP, geen IP-lookup en geen automatische detectie; de gebruiker kan optioneel eenregiouit eenKeuzelijstkiezen. Zie Beveiliging & privacy.
Frontend: React-monorepo
Section titled “Frontend: React-monorepo”De frontend is een greenfield React 19 + Vite + TypeScript-monorepo met twee apps en gedeelde libs. De UI draait binnen een mobiel kader en is meertalig-klaar (nl vandaag; en/es/fr als doelbeeld).
Monorepo-structuur
Section titled “Monorepo-structuur”Gebouwd (per 2026-07-09): app/ is een npm-workspaces-monorepo met drie packages:
app/ # npm workspaces (root package.json met lint/typecheck/test/build-scripts)├─ frontend/ # gebruikersportaal (React 19 + Vite, i18next, vitest-unittests, Playwright-e2e)├─ beheer/ # beheerportaal (React 19 + Vite)└─ shared/ # @iseeyou/shared — gedeeld tussen beide portalen| Package | Inhoud |
|---|---|
shared (@iseeyou/shared) | De Toegangspoort, de api-basis (fetch-wrapper, bearer-tokenbeheer, uniform 401-gedrag), design-tokens (Tailwind v4 @theme), gedeelde TypeScript-types van de API-responses, helpers (niveau-labels, datum, sectie-/attribuutlabels), de ErrorBoundary en de centrale localStorage-sleutels. |
frontend | Gebruikersportaal-schermen, i18next-catalogus (nl), camera-hook (useCameraOpname voor getUserMedia/MediaRecorder), lazy-geladen schermen. |
beheer | Beheerportaal (SPA-schil met zijbalk, hash-routing, stale-while-revalidate-datacache). |
Het doelbeeld hieronder blijft richtinggevend voor de volgende stappen — met name de uit OpenAPI gegenereerde API-client, de SignalR-client, TanStack Query en de extra talen:
| Lib (doelbeeld) | Inhoud |
|---|---|
libs/ui | Gedeelde, toegankelijke componenten op basis van Tailwind CSS en Headless UI/Radix. |
libs/api | De uit OpenAPI gegenereerde getypeerde API-client en de @microsoft/signalr-client. |
libs/shared | Gedeelde TypeScript-types, i18next-configuratie, en hooks — waaronder een getypeerde camera-hook voor getUserMedia/MediaRecorder (video 10s / foto) en locale-detectie via navigator.language. |
Details over routes, state en componenten staan op Frontend; media-opname op Media & opname.
Tech stack — backend
Section titled “Tech stack — backend”| Onderwerp | Keuze | Toelichting |
|---|---|---|
| Taal & platform | .NET / C# | Gelaagd + DDD, modulaire monoliet, module per bounded context. |
| Web/API | ASP.NET Core Minimal API | Endpoints in ISeeYou.Api; compositieroot via DI. |
| Use-cases | Expliciete handlers | Dunne handlers in ISeeYou.Application; geen MediatR. |
| ORM & database | EF Core + PostgreSQL (Npgsql) | Owned types, jsonb voor scores, value conversions, check- en unieke constraints, xmin-concurrency. |
| Realtime | SignalR | AanwezigheidHub + BeoordelingHub; Redis-backplane bij scale-out. |
| Auth (beheer) | ASP.NET Core Identity | Gehashte wachtwoorden, MFA mogelijk, rol Beheerder. |
| Auth (gebruiker) | Custom anoniem | Eigen Gebruiker/Sessie-aggregate, Argon2id, fictieve naam; optioneel e-mailadres, uitsluitend voor wachtwoordherstel. |
| Achtergrondwerk | BackgroundService | O.a. RetentieHostedService (altijd actief) en opschoning van vluchtige data. |
| Media-opslag | IMediaStore | S3-compatibele implementatie (AWS SDK for .NET) tegen S3/GCS/MinIO; filesystem/MinIO lokaal. |
| AI-persona (vangnet) | IAiPersonaService → OpenAI gpt-4o-mini (vision) | Beoordeelt een Open slot wanneer er geen mens (op tijd) levert of de beheerder er handmatig om vraagt (Opinie.bron = AI). Vision-gebaseerd op het goedkope gpt-4o-mini: foto rechtstreeks, video via ~4 FFmpeg-frames; scoort alleen de gekozen secties — allemaal, verplicht (1–5 per 2026-07-05; zo gebouwd) — gestuurd door de persona-instructie. Config OpenAI__ApiKey (nu ook chat/completions-permissie); fail-graceful terugval op deterministische scores. Kosten < $0,01 per opinie. |
| Video-frames voor AI | FFmpeg (in het API-Docker-image) | Haalt ~4 frames uit een video (max. 10s) zodat het vision-model de opname kan “zien”; alleen nodig voor de AI-persona bij MediaType.Video. Zie Media & opname. |
| Tekstmoderatie | IModeratieDienst → OpenAiModeratieDienst | Controleert alle vrije tekst (toelichting, reactie) via de OpenAI Moderation API (omni-moderation-latest, gratis); config OpenAI__ApiKey (Moderations-permissie), fail-open zonder sleutel. Dezelfde sleutel bedient de vision-AI-persona (die daarnaast de chat/completions-permissie vereist). |
| Uitslagnotificatie | IUitslagNotificaties → ACS Email | Stuurt zodra de uitslag klaar is één mail met alleen een link (uitslagnotificatie) via Azure Communication Services Email (afzender DoNotReply@…azurecomm.net); config Email__ConnectionString, Email__Afzender, App__PortaalUrl; fail-graceful zonder configuratie of bij een fout. Adres wordt na de verzendpoging gewist. |
Betalingen (iDeal) (vastgesteld 2026-07-08b; PSP gekozen 2026-07-11h) | Mollie (iDeal) achter IBetaalPoort | Betaald advies loopt via een PSP: iDeal-checkout voor de inleg en teaser-ontgrendeling, met webhook-bevestiging (idempotent verwerkt). De gekozen PSP is Mollie (ADR-035): iDeal-thuisspel, NL-eerst, EUR, laagste frictie. De webhook vraagt de status server-to-server bij Mollie op (GET /v2/payments/{id}) en vertrouwt de webhook-body niet; IBetaalPoort is daarom async met een status-opvraag. Achter de applicatie-poort blijft de PSP inwisselbaar. Het platform bewaart geen bank-/kaartgegevens — alleen transactiereferentie + saldo; betaalgegevens leven bij de PSP. De gesimuleerde (dummy) iDeal blijft als terugval voor lokaal/CI (zonder Mollie:ApiKey). De uitbetaling aan beoordelaars (KYC/DAC7) is nog niet geautomatiseerd — latere story; de poort houdt Mollie Connect/Stripe/Adyen daarvoor open. |
| Regio | Keuzelijst (geen GeoIP, geen landveld) | Optionele, door de gebruiker gekozen regio uit een vlakke Keuzelijst (geen landprefix meer sinds 2026-07-11m); geen IP-lookup of detectie. IP blijft alleen voor rate-limiting. |
| Systeeminstellingen | Systeem (Systeeminstellingen) | Via Beheer instelbare parameters (o.a. het maximum aantal beoordelingen per vraag, antwoordtermijn, creditkostenPerBeoordeling + creditsPerBeoordeling, meldingen-drempel, online-venster, veiligheids-timeout). |
| Beveiliging | Rate limiting, CORS, HSTS, CSP | Ingebouwde .NET rate limiting (policies api/opinie/melding), CORS-allowlist, HTTPS/HSTS, krappe CSP, body-limieten. |
| Tests | xUnit, Testcontainers-Postgres | Domein/applicatie met xUnit; integratie/endpoints/mapping met Testcontainers; SignalR-testclient. |
Tech stack — frontend
Section titled “Tech stack — frontend”| Onderwerp | Keuze | Toelichting |
|---|---|---|
| Framework | React 19 | In een monorepo met twee apps + gedeelde libs. |
| Build | Vite | Dev-server met HMR; productie-build naar statische assets. |
| Taal | TypeScript | Door de hele monorepo. |
| Monorepo | pnpm workspaces of Turborepo | apps/{beheerportaal,gebruikersportaal} + libs/{ui,api,shared}. |
| Styling | Tailwind CSS | Utility-classes; mobiel-kader-layout. |
| Componenten | Headless UI / Radix | Toegankelijke, ongestileerde basiscomponenten. |
| Routing | React Router | Client-side navigatie per app. |
| Server-state | TanStack Query | Caching/synchronisatie van server-data. |
| Formulieren | React Hook Form + Zod | Formulierbeheer en schema-validatie. |
| i18n | i18next | Meertalig: nl, en, es, fr. |
| Locale-detectie | navigator.language | Detecteert de browsertaal als hint voor de UI-taal (geen locatie); corrigeerbaar door de gebruiker. |
| Regio-lijst | Beheerbare stamdata | Vlakke regio-lijst (Keuzelijst) waaruit de gebruiker optioneel kiest; geen vrij invoerveld, geen detectie, geen landveld meer. |
| API-client | Uit OpenAPI gegenereerd | Getypeerde client in libs/api. |
| Realtime-client | @microsoft/signalr | Doelbeeld: verbindt met de BeoordelingHub. Nog niet gebouwd — de portalen pollen nu (de backend-hub bestaat al). |
| Camera | Getypeerde camera-hook | getUserMedia/MediaRecorder voor video 10s / foto. |
| Tests | Vitest + Testing Library, Playwright | Unit/component met Vitest; e2e met Playwright. |
Versies worden pas bij de implementatie vastgepind (cloud- en versie-agnostisch), met de regel lange-termijn stabiliteit boven snelheid.
Van prototype-stores naar EF Core-entiteiten
Section titled “Van prototype-stores naar EF Core-entiteiten”Het prototype (referentie) bewaarde alles in JSON-stores. In app/ worden deze vervangen door EF Core-entiteiten in PostgreSQL, ingedeeld per bounded context. De mapping:
| Store (prototype) | EF Core-entiteit | Bounded context |
|---|---|---|
gebruikers.json | Gebruiker | Gebruiker & Toegang |
tempgebruikers.json | AnoniemeGebruiker | Gebruiker & Toegang |
sessies.json | Sessie | Gebruiker & Toegang |
videos.json / tempvideos.json | Vraag (video/foto, incl. tijdelijke buffer) | Beoordeling |
opdrachten.json | Opdracht | Beoordeling |
opinies.json | Opinie | Beoordeling |
reports.json | Melding | Moderatie & Retentie |
berichten.json | Bericht | Beheer |
keuzelijsten.json | Keuzelijst | Beheer |
systeem.json | Systeem | Beheer |
De
Keuzelijstis inapp/de beheerbare stamdata: gender, de opties voor culturele achtergrond, de secties/attributen (met Gezicht/Kleding/Accessoires als standaard) én de vlakkeregio-lijst. (Het typeland— de landenlijst — is per2026-07-11mvervallen, samen met hetLand-veld — ADR-037.)
Het prototype kende geen financiële store. Credits zijn een teller op de
Gebruiker/AnoniemeGebruiker; de waardering (0–5) die de maker aan een ontvangen beoordeling geeft, staat op deOpiniemet afgeleide statistieken (aantalBeoordelingen,gemiddeldeWaardering) op de reviewer. (Vastgesteld per2026-07-08ben gebouwd — dummy-iDeal:) de context Beloning & Financieel voegt daar nieuwe entiteiten aan toe —Ster,Betaling(iDeal via de PSP) enUitbetaling— pluseuroSaldo/aantalSterren/adviesniveauop deGebruiker; de ster-loterij blijft geschrapt. Zie Waardering & reviews en Betaald advies.
Hosting & lokale ontwikkeling
Section titled “Hosting & lokale ontwikkeling”De doelhosting is cloud-agnostisch met lage startkosten en ruimte om op te schalen: de API als Docker-image op een serverless-container-platform met scale-to-zero (lage kosten bij weinig verkeer) en autoscaling (klaar voor pieken), met managed PostgreSQL, S3-compatibele object-storage en — bij opschalen — Redis als SignalR-backplane. Geen cloud-lock-in; platforms als Cloud Run, App Runner/Fargate of Container Apps zijn neutrale voorbeelden. Lokale ontwikkeling via Docker Compose (optioneel .NET Aspire). Zie Deployment.
Samenhang
Section titled “Samenhang”flowchart LR
subgraph Repo["repo (branch main)"]
Docs["docs/ — Astro Starlight (spec)"]
subgraph App["app/"]
FE["frontend/ — React 19 + Vite (2 apps + libs)"]
BE["backend/ — .NET modulaire monoliet (DDD)"]
end
Bron["bron/ — functioneel ontwerp + prototype (referentie)"]
end
Docs -->|is spec voor| App
Bron -.->|referentie voor| App
FE -->|REST via OpenAPI-client| BE
FE -->|SignalR push| BE
BE -->|EF Core/Npgsql| DB[("PostgreSQL")]
BE -->|IMediaStore| Blob[("S3-compatibele object-storage")]
BE -->|IAiPersonaService vangnet| AI[("AI-persona (OpenAI gpt-4o-mini vision + FFmpeg)")]
Documentatiesite (deze site)
Section titled “Documentatiesite (deze site)”De documentatie in docs/ is een Astro + Starlight-site. Diagrammen zijn Mermaid en renderen automatisch tijdens de build (geen Java/PlantUML nodig). Lokaal: cd docs && npm run dev (http://localhost:4321/). De site wordt gebouwd met npm run build en gehost als statische site. Zie Deployment.