Ga naar inhoud

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.

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.

MapRolStatus
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/

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:

  1. Werk eerst docs/ bij — het domeinmodel, de termen en het/de geraakte scenario(‘s).
  2. Bump de modelversie en voeg een changelog-entry toe.
  3. Bouw en controleer (cd docs && npm run build): diagrammen renderen, geen kapotte links.
  4. 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).

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.

ProjectLaagVerantwoordelijkheid
ISeeYou.DomainDomein (puur)Aggregates, entities, value objects en domeinregels per bounded context. Geen frameworks, geen I/O.
ISeeYou.ApplicationApplicatieUse-cases als dunne, expliciete handlers (geen MediatR). Definieert de poorten: IRepository<T>, IMediaStore, IRealtimeNotifier, IClock, IPasswordHasher, IAiPersonaService, IModeratieDienst, IUitslagNotificaties.
ISeeYou.InfrastructureInfrastructuurImplementaties: 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.ApiAPI / compositierootMinimal API-endpoints, SignalR-hubs en de DI-compositieroot die alles bekabelt.
test/*TestsxUnit voor domein/applicatie; Testcontainers-Postgres voor integratie/endpoints/mapping; SignalR-testclient.

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.Application definieert poorten (IRepository<T>, IMediaStore, IRealtimeNotifier, IClock, IPasswordHasher, IAiPersonaService, IModeratieDienst, IUitslagNotificaties); ISeeYou.Infrastructure implementeert ze; ISeeYou.Api koppelt 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).

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

PortaalDekt grofweg
BeheerportaalContext Beheer + de admin-kant van Moderatie & Retentie
GebruikersportaalBeoordeling (incl. credits en waarderen) + melden + de gebruikerskant van Gebruiker & Toegang

Geen externe identity-provider; auth zit in-app en is afgestemd op de anonimiteitsbelofte.

DoelgroepMechanisme
BeheerdersASP.NET Core Identity (in-app, gehashte wachtwoorden, MFA mogelijk, rol Beheerder).
GebruikersCustom 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.

  • EF Core + PostgreSQL (Npgsql). Value objects als owned types; Scores (op basis van de beheerbare secties/attributen) als jsonb en de optionele reactie als nullable tekstkolom; enums via value conversion naar string; Score 1..5 (per 2026-07-05; zo gebouwd) en Waardering 0..5 met check-constraint. De reviewer-waarderingsstatistieken (aantalBeoordelingen, gemiddeldeWaardering) staan als tellers op de Gebruiker; het credits-saldo (>= 0) idem. Optimistic concurrency via xmin. Migrations via dotnet 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-events OpdrachtBeschikbaar (nieuw werk in de pool) en VraagBeoordeeld (uitslag klaar); REST voor alle state-mutaties, SignalR alleen voor push. De frontend pollt nu nog (de SignalR-client is een follow-up); een aparte AanwezigheidHub voor 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-actieve RetentieHostedService : BackgroundService. Zie Backend & API — Persistentie en Media & opname.
  • Geen landveld, geen locatie-tracking. (Per 2026-07-11m, ADR-037) is er geen Land-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 een regio uit een Keuzelijst kiezen. Zie Beveiliging & privacy.

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

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
PackageInhoud
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.
frontendGebruikersportaal-schermen, i18next-catalogus (nl), camera-hook (useCameraOpname voor getUserMedia/MediaRecorder), lazy-geladen schermen.
beheerBeheerportaal (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/uiGedeelde, toegankelijke componenten op basis van Tailwind CSS en Headless UI/Radix.
libs/apiDe uit OpenAPI gegenereerde getypeerde API-client en de @microsoft/signalr-client.
libs/sharedGedeelde 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.

OnderwerpKeuzeToelichting
Taal & platform.NET / C#Gelaagd + DDD, modulaire monoliet, module per bounded context.
Web/APIASP.NET Core Minimal APIEndpoints in ISeeYou.Api; compositieroot via DI.
Use-casesExpliciete handlersDunne handlers in ISeeYou.Application; geen MediatR.
ORM & databaseEF Core + PostgreSQL (Npgsql)Owned types, jsonb voor scores, value conversions, check- en unieke constraints, xmin-concurrency.
RealtimeSignalRAanwezigheidHub + BeoordelingHub; Redis-backplane bij scale-out.
Auth (beheer)ASP.NET Core IdentityGehashte wachtwoorden, MFA mogelijk, rol Beheerder.
Auth (gebruiker)Custom anoniemEigen Gebruiker/Sessie-aggregate, Argon2id, fictieve naam; optioneel e-mailadres, uitsluitend voor wachtwoordherstel.
AchtergrondwerkBackgroundServiceO.a. RetentieHostedService (altijd actief) en opschoning van vluchtige data.
Media-opslagIMediaStoreS3-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 AIFFmpeg (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.
TekstmoderatieIModeratieDienstOpenAiModeratieDienstControleert 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).
UitslagnotificatieIUitslagNotificaties → ACS EmailStuurt 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 IBetaalPoortBetaald 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.
RegioKeuzelijst (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.
SysteeminstellingenSysteem (Systeeminstellingen)Via Beheer instelbare parameters (o.a. het maximum aantal beoordelingen per vraag, antwoordtermijn, creditkostenPerBeoordeling + creditsPerBeoordeling, meldingen-drempel, online-venster, veiligheids-timeout).
BeveiligingRate limiting, CORS, HSTS, CSPIngebouwde .NET rate limiting (policies api/opinie/melding), CORS-allowlist, HTTPS/HSTS, krappe CSP, body-limieten.
TestsxUnit, Testcontainers-PostgresDomein/applicatie met xUnit; integratie/endpoints/mapping met Testcontainers; SignalR-testclient.
OnderwerpKeuzeToelichting
FrameworkReact 19In een monorepo met twee apps + gedeelde libs.
BuildViteDev-server met HMR; productie-build naar statische assets.
TaalTypeScriptDoor de hele monorepo.
Monorepopnpm workspaces of Turborepoapps/{beheerportaal,gebruikersportaal} + libs/{ui,api,shared}.
StylingTailwind CSSUtility-classes; mobiel-kader-layout.
ComponentenHeadless UI / RadixToegankelijke, ongestileerde basiscomponenten.
RoutingReact RouterClient-side navigatie per app.
Server-stateTanStack QueryCaching/synchronisatie van server-data.
FormulierenReact Hook Form + ZodFormulierbeheer en schema-validatie.
i18ni18nextMeertalig: nl, en, es, fr.
Locale-detectienavigator.languageDetecteert de browsertaal als hint voor de UI-taal (geen locatie); corrigeerbaar door de gebruiker.
Regio-lijstBeheerbare stamdataVlakke regio-lijst (Keuzelijst) waaruit de gebruiker optioneel kiest; geen vrij invoerveld, geen detectie, geen landveld meer.
API-clientUit OpenAPI gegenereerdGetypeerde client in libs/api.
Realtime-client@microsoft/signalrDoelbeeld: verbindt met de BeoordelingHub. Nog niet gebouwd — de portalen pollen nu (de backend-hub bestaat al).
CameraGetypeerde camera-hookgetUserMedia/MediaRecorder voor video 10s / foto.
TestsVitest + Testing Library, PlaywrightUnit/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-entiteitBounded context
gebruikers.jsonGebruikerGebruiker & Toegang
tempgebruikers.jsonAnoniemeGebruikerGebruiker & Toegang
sessies.jsonSessieGebruiker & Toegang
videos.json / tempvideos.jsonVraag (video/foto, incl. tijdelijke buffer)Beoordeling
opdrachten.jsonOpdrachtBeoordeling
opinies.jsonOpinieBeoordeling
reports.jsonMeldingModeratie & Retentie
berichten.jsonBerichtBeheer
keuzelijsten.jsonKeuzelijstBeheer
systeem.jsonSysteemBeheer

De Keuzelijst is in app/ de beheerbare stamdata: gender, de opties voor culturele achtergrond, de secties/attributen (met Gezicht/Kleding/Accessoires als standaard) én de vlakke regio-lijst. (Het type land — de landenlijst — is per 2026-07-11m vervallen, samen met het Land-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 de Opinie met afgeleide statistieken (aantalBeoordelingen, gemiddeldeWaardering) op de reviewer. (Vastgesteld per 2026-07-08b en gebouwd — dummy-iDeal:) de context Beloning & Financieel voegt daar nieuwe entiteiten aan toe — Ster, Betaling (iDeal via de PSP) en Uitbetaling — plus euroSaldo/aantalSterren/adviesniveau op de Gebruiker; de ster-loterij blijft geschrapt. Zie Waardering & reviews en Betaald advies.

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.

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)")]

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.