Ga naar inhoud

Beslissingen (ADR's)

Deze pagina legt de belangrijkste architectuurbeslissingen voor I See You vast als Architecture Decision Records (ADR’s). Elke ADR beschrijft compact de context (waarom de keuze speelt), de beslissing, de status en de gevolgen. De beslissingen beschrijven de greenfield-doelarchitectuur: backend én frontend worden vanaf 0 opgebouwd. Het oude prototype (bron/prototype/) is uitsluitend referentie/uitgangspunt, niet de basis.

Domeinbegrippen verwijzen naar Termen en het Domeinmodel (de context die voorheen “Beheer & Referentie” heette, heet nu Beheer). De keuzes zijn telkens onderbouwd op de eisen van I See You: lage startkosten, schaalbaarheid, anonimiteit en een camera-centrisch (live capture) ontwerp.

ADRTitelStatus
ADR-001.NET / C# als platformAanvaard
ADR-002PostgreSQL + EF Core als persistentieAanvaard (vervangt JSON + cache)
ADR-003Gelaagd + DDD, modulaire monolietAanvaard
ADR-004Geen MediatRAanvaard
ADR-005Vluchtig + persistent in dezelfde DBAanvaard
ADR-006SignalR voor push, REST voor mutatiesAanvaard
ADR-007Geen Keycloak; Identity + anonieme authAanvaard
ADR-008IMediaStore, cloud-agnostische opslagAanvaard
ADR-009Foto en video in één media-pipelineAanvaard
ADR-010Retentie altijd actiefAanvaard
ADR-011Single-device-sessiesAanvaard
ADR-012React 19 + Vite + TypeScriptAanvaard
ADR-013Twee portalen op één APIAanvaard
ADR-014Single-tenantAanvaard
ADR-015Cloud-agnostische hosting, scale-to-zeroAanvaard
ADR-016Land als keuzelijst (ISO 3166-1) + afgeleide UN M49-regioAanvaard (vervangt GeoIP-locatie)
ADR-017Credits als deelname-poort, instelbare parameters, online als voorrang, AI-persona-vangnetAanvaard (credits-poort vervangt reciprociteit)
ADR-018Credits ≠ sterren (gescheiden valuta)Ingetrokken (sterren geschrapt, zie ADR-019)
ADR-0190–5 kwaliteitswaardering vervangt loterij-ster + uitbetalingAanvaard (vervangt ADR-018), deels overruled per 2026-07-05: de waardering blijft (en weegt nu mee in de ster-ranking), maar het “geen geld / geen ster / geen uitbetaling”-deel is overruled door ADR-027ADR-029 — de ster keert terug als verdiende reputatie (géén loterij), met betaald advies, euro-saldo/iDeal en uitbetaling (stakeholder-notities Han 3-7-2026, ter verificatie)
ADR-020OpenAI-tekstmoderatie (gratis, fail-open)Aanvaard
ADR-021Open vragen/antwoorden i.p.v. een algemeen opmerkingenveldAanvaard, herzien (2026-07-04d): de open vragen/antwoorden zijn vervallen; elke kant houdt precies één vrije tekst — de maker de toelichting, de opiniegever één reactie (Opinie.reactie) — beide door de tekstmoderatie. Beoordelingsselectie bevat alleen nog secties (een vraag is geldig bij ≥1 sectie); het algemene opmerkingenveld blijft verwijderd (stakeholder-review 2-7-2026: één tekst per kant is eenvoudiger uit te leggen én te modereren; met een goede reactie kan de reviewer zich onderscheiden voor een hoge waardering)
ADR-022Instelbaar aantal beoordelingen met schalende kostenAanvaard, herzien (2026-07-04): het aantal beoordelingen per vraag is een vaste beheerinstelling (aantalOpiniegevers, op de vraag bevroren als aantalBeoordelingen); de keuze door de maker — en daarmee de per vraag schalende prijs — is vervallen (stakeholder-review 2-7-2026: voelde commercieel, onduidelijk voor nieuwe gebruikers)
ADR-023Uitslagnotificatie per e-mail: alleen een link, via Azure Communication ServicesAanvaard (2026-07-04e)
ADR-024Vision-gebaseerde AI-persona op gpt-4o-mini + handmatige AI-opinieAanvaard (2026-07-04f)
ADR-025Maker bepaalt de vrije reactie (verplicht wanneer gevraagd); AI geeft nu een reactieAanvaard (2026-07-04g); het maker-keuze-deel is per 2026-07-10h herzien — vrije reacties komen uit vrije-reactie-slots voor niveau-beoordelaars (ontwerpbeslissing 34, issue #17); het AI-reactie-deel blijft gelden
ADR-026Standaard 1 beoordeling; AI-herkomst verborgen voor de maker; elke beoordeling waardeerbaarAanvaard (2026-07-04h); het default-deel is per 2026-07-05 teruggedraaid naar 10 (zie ADR-030); de AI-herkomst- en waardeerbaarheids-delen blijven gelden
ADR-027Sterren als verdiende kwaliteitsreputatie via de consensus-rankingVastgesteld (2026-07-08b, geverifieerd door Han; geïmplementeerd 2026-07-08)
ADR-028Betaald advies met adviesniveaus en de teaser-flowVastgesteld (2026-07-08b, geverifieerd door Han; geïmplementeerd 2026-07-08)
ADR-029Euro-saldo met iDeal (via een PSP) en eenmalige welkomstbonus — geld keert terugVastgesteld (2026-07-08b, geverifieerd door Han; overrulet het “geen geld”-deel van ADR-019)
ADR-030Onboarding met eerst-2-beoordelen-poort (met vangrail) en default terug naar 10Vastgesteld (2026-07-08b, geverifieerd door Han; geïmplementeerd 2026-07-08)
ADR-031In-app auth met ondertekende bearer-tokens (Authorization-header i.p.v. cookies)Aanvaard (2026-07-08); vervangt de cookievorm van ADR-007/011 voor de cross-origin-topologie
ADR-032Observability: Serilog-gestructureerde logging + Application InsightsAanvaard (2026-07-11)
ADR-033CI/CD op een self-hosted runner (build, test én automatische deploy naar Azure)Aanvaard (2026-07-11)
ADR-034Visuele moderatie van opnames vóór distributie (fail-open)Aanvaard (2026-07-11g)
ADR-035Mollie als PSP-implementatie; betaalpoort async + status-opvraagAanvaard (2026-07-11h)
ADR-036First-party product-analytics (cookieloos, zonder PII, geen externe dienst)Aanvaard (2026-07-11)
ADR-037Land vervalt (single-tenant per site); Regio wordt een vlakke keuzelijstAanvaard (2026-07-11m)
ADR-038Audittrail voor beheer-support (mét actor-identiteit, admin-only)Aanvaard (2026-07-11n)
ADR-039Infrastructure as Code met Pulumi; meerdere omgevingen (voorstel)Gedeeltelijk aanvaard (2026-07-11o): IaC-codificatie van de bestaande productieomgeving is gebouwd; deel 2 (aparte resourcegroep per omgeving) is Vervangen door ADR-044
ADR-040Login-normalisatie (getrimd + hoofdletter-ongevoelig) en uniek e-mailadresAanvaard (2026-07-13)
ADR-041Client-side crash-rapportage (first-party, cookieloos) + auto-reload bij verouderde chunksAanvaard (2026-07-13)
ADR-042Beheerbare e-mailteksten per mailtype, met placeholder-contractAanvaard (2026-07-13c)
ADR-043Eigen beoordelingscriteria: open vragen op de smiley-schaalAanvaard (2026-07-13d)
ADR-044Meerdere omgevingen in één resourcegroep, met persistente DNS-zones en custom domainsAanvaard (2026-07-14b); vervangt deel 2 van ADR-039

Statuswaarden. Voorgesteld = nog te bevestigen; Aanvaard = vastgelegd en leidend voor de implementatie; Vervangen/Ingetrokken = opgevolgd door of geschrapt ten gunste van een latere ADR.

De vier drijfveren van I See You — lage kosten, schaalbaarheid, anonimiteit en camera-centrisch — verklaren waarom de meeste ADR’s zijn zoals ze zijn. Sommige ADR’s dienen vooral structuur/kwaliteit (typeveiligheid, onderhoudbaarheid).

flowchart LR
    Kosten["Lage kosten"]
    Schaal["Schaalbaarheid"]
    Anon["Anonimiteit"]
    Camera["Camera-centrisch (live capture)"]
    Struct["Structuur en kwaliteit"]

    Struct --> A1["ADR-001 .NET/C#"]
    Struct --> A3["ADR-003 DDD modulaire monoliet"]
    Struct --> A4["ADR-004 Geen MediatR"]

    Anon --> A2["ADR-002 PostgreSQL + EF Core"]
    Schaal --> A2
    Anon --> A5["ADR-005 Vluchtig en persistent in 1 DB"]
    Kosten --> A5

    Schaal --> A6["ADR-006 SignalR push, REST mutaties"]
    Kosten --> A7["ADR-007 Identity + anonieme auth"]
    Anon --> A7

    Kosten --> A8["ADR-008 IMediaStore S3-compatibel"]
    Camera --> A9["ADR-009 Eén media-pipeline (MediaType)"]
    Anon --> A10["ADR-010 Retentie altijd aan"]
    Anon --> A11["ADR-011 Single-device-sessies"]

    Camera --> A12["ADR-012 React 19 + Vite + TS"]
    Schaal --> A13["ADR-013 Twee portalen, 1 API"]
    Kosten --> A14["ADR-014 Single-tenant"]
    Kosten --> A15["ADR-015 Scale-to-zero hosting"]
    Schaal --> A15

    Anon --> A16["ADR-016 Land als keuzelijst (ISO 3166-1 + afgeleide UN M49)"]
    Kosten --> A16

    Schaal --> A17["ADR-017 Credits-poort, instelbare parameters, online-voorrang, AI-vangnet"]
    Anon --> A17

    Struct --> A19["ADR-019 0-5 waardering vervangt loterij-ster en uitbetaling"]
    Anon --> A20["ADR-020 OpenAI-tekstmoderatie (gratis, fail-open)"]
    Struct --> A21["ADR-021 Eén vrije tekst per kant: toelichting en reactie (herzien 2026-07-04d)"]
    Schaal --> A22["ADR-022 Aantal beoordelingen per vraag als beheerinstelling (herzien 2026-07-04)"]
    Anon --> A23["ADR-023 Uitslagnotificatie: alleen een link via ACS, fail-graceful, adres gewist"]
    Kosten --> A23

    Camera --> A24["ADR-024 Vision-AI-persona op gpt-4o-mini + FFmpeg, handmatige AI-opinie, fail-graceful"]
    Kosten --> A24

    Struct --> A25["ADR-025 Maker bepaalt vrije reactie (verplicht wanneer gevraagd); AI geeft nu een reactie"]
    Camera --> A25

    Struct --> A26["ADR-026 Standaard 1 beoordeling; AI-herkomst verborgen voor de maker; elke beoordeling waardeerbaar"]
    Anon --> A26

    Struct --> A27["ADR-027 Sterren als verdiende reputatie via consensus-ranking (voorgesteld)"]
    Schaal --> A28["ADR-028 Betaald advies met adviesniveaus en teaser-flow (voorgesteld)"]
    Kosten --> A29["ADR-029 Euro-saldo, iDeal via PSP en welkomstbonus (voorgesteld)"]
    Anon --> A29
    Schaal --> A30["ADR-030 Onboarding-poort met vangrail; default terug naar 10 (voorgesteld)"]

Context. Het prototype is gebouwd op een Node/Express-backend met JSON-bestanden en vlakke JavaScript. Voor de greenfield-herbouw is een platform nodig dat een gelaagde, domeingedreven opzet goed ondersteunt en dat fouten vroeg afvangt. De keuze wordt gemaakt op technische merites, niet op een bedrijfsstandaard.

Beslissing. De backend wordt gebouwd op .NET / C#. Doorslaggevend zijn de statische typeveiligheid (value objects, enums, domeininvarianten dwingbaar in het typesysteem), een rijk ecosysteem voor de bouwstenen die dit project nodig heeft (EF Core, ASP.NET Core Minimal API, SignalR, BackgroundServices, ingebouwde rate limiting), en goede ondersteuning voor een gestructureerde, gelaagde architectuur (zie ADR-003).

Status. Aanvaard.

Gevolgen.

AspectGevolg
VoordeelSterke typeveiligheid: domeininvarianten en value objects worden door de compiler bewaakt i.p.v. door losse runtime-checks.
VoordeelEén ecosysteem dekt API (Minimal API), realtime (SignalR), achtergrondtaken (BackgroundService) en data (EF Core) zonder losse stukjes te lijmen.
GevolgDe prototype-JS wordt niet hergebruikt; het dient als functionele referentie, niet als codebasis.
BeperkingHet team heeft C#/.NET-kennis nodig; dat is een bewuste investering in onderhoudbaarheid.

Zie Architectuur — Backend & API.


ADR-002 — PostgreSQL + EF Core als persistentie

Section titled “ADR-002 — PostgreSQL + EF Core als persistentie”

Context. Het prototype bewaarde data in JSON-bestanden met een in-memory cache. Dat schaalt niet horizontaal (één proces bezit de cache), kent geen transacties of relationele integriteit, en maakt het lastig om invarianten af te dwingen. I See You moet schaalbaar zijn en harde regels (bv. score 1..5, waardering 0..5) kunnen garanderen. Deze beslissing vervangt de oude JSON + cache-keuze.

Beslissing. Persistentie gaat via EF Core met PostgreSQL (Npgsql). Value objects worden owned types; de 28 attribuut-Scores gaan als jsonb; enums via value conversion naar string; Score 1..5 wordt als domeininvariant afgedwongen (de scores staan als jsonb) en de waardering krijgt een check-constraint 0..5; de reviewer-GemiddeldeWaardering is afgeleid uit WaarderingSom/WaarderingAantal. Optimistic concurrency via xmin. Schema-evolutie via dotnet ef-migrations; de Keuzelijst wordt geseed.

Status. Aanvaard (vervangt JSON + cache).

Gevolgen.

AspectGevolg
VoordeelEchte transacties en relationele integriteit; invarianten zoals de score-range 1..5, de waardering 0..5 en een niet-negatief credit-saldo worden door de database (check-constraints) of het domein afgedwongen, niet door applicatielogica alleen.
VoordeelHorizontaal schaalbaar: meerdere stateless API-instances delen één database (vereiste voor ADR-013/ADR-015).
Voordeeljsonb voor de 28 scores houdt het schema compact zonder 28 kolommen of join-tabellen.
BeperkingVereist een (managed) PostgreSQL-instantie; meer operationele zorg dan losse JSON-bestanden, maar onmisbaar voor schaal en integriteit.
GevolgMediabytes staan niet in Postgres; de DB bewaart alleen een key + metadata (zie ADR-008).

Zie Architectuur — Backend & API.


ADR-003 — Gelaagd + DDD, modulaire monoliet

Section titled “ADR-003 — Gelaagd + DDD, modulaire monoliet”

Context. Het prototype was een vlakke server.js zonder duidelijke laaggrenzen. De bounded contexts (Beoordeling, Gebruiker & Toegang, Beloning & Financieel, Moderatie & Retentie, Beheer) verdienen elk een eigen, consistente module — maar een verzameling losse microservices zou voor dit project onnodige kosten en operationele complexiteit opleveren.

Beslissing. De backend is een gelaagde, domeingedreven (DDD) modulaire monoliet met één module per bounded context. Projecten met afhankelijkheden strikt naar binnen (Domain ← Application ← Infrastructure ← Api):

  • ISeeYou.Domain — puur domein (aggregates, value objects, invarianten), geen frameworkverwijzingen.
  • ISeeYou.Application — use-cases + poorten: IRepository<T>, IMediaStore, IRealtimeNotifier, IClock, IPasswordHasher.
  • ISeeYou.Infrastructure — EF Core/Npgsql, media, hashing, BackgroundServices.
  • ISeeYou.Api — Minimal API + SignalR-hubs, compositieroot.
  • test/* — domein-, applicatie- en integratietests.

Status. Aanvaard.

Gevolgen.

AspectGevolg
VoordeelHeldere modulegrenzen per context, maar één deployable: lage operationele kosten, geen netwerk-overhead tussen contexten.
VoordeelDe afhankelijkheidsrichting houdt het domein puur en testbaar; poorten (interfaces) maken Infrastructure verwisselbaar.
VoordeelStateless ontworpen, klaar voor horizontale schaal (ADR-006/ADR-015).
BeperkingModulegrenzen moeten bewust bewaakt worden; een monoliet maakt het verleidelijk om grenzen te overschrijden.
UitbouwEen module kan later met beperkte moeite worden afgesplitst als één context echt apart moet schalen.

Zie Architectuur — Backend & API.


ADR-004 — Geen MediatR; dunne, expliciete use-case-handlers

Section titled “ADR-004 — Geen MediatR; dunne, expliciete use-case-handlers”

Context. Een veelgebruikte manier om use-cases te ontkoppelen is een in-process mediator (zoals MediatR). Dat voegt echter een indirectielaag, runtime-reflectie en een externe dependency toe, terwijl de use-cases van I See You overzichtelijk en expliciet kunnen blijven.

Beslissing. Geen MediatR. Use-cases worden dunne, expliciete handlers in de Application-laag die rechtstreeks via de poorten (IRepository<T>, IMediaStore, IRealtimeNotifier, IClock, IPasswordHasher) worden aangeroepen vanuit de Api-laag. De afhandeling is daarmee direct te volgen in de code, zonder verborgen dispatch.

Status. Aanvaard.

Gevolgen.

AspectGevolg
VoordeelAanroep is expliciet en goed te volgen (geen “magische” dispatch via reflectie).
VoordeelEén afhankelijkheid minder; minder ceremonie voor een project van deze omvang.
BeperkingCross-cutting gedrag (logging, validatie) wordt expliciet toegevoegd i.p.v. via mediator-pipelines; voor dit project een acceptabele afweging.
ConventieEén handler per use-case, met duidelijke invoer/uitvoer; de Api-laag is de compositieroot.

Zie Architectuur — Backend & API.


ADR-005 — Vluchtig én persistent in dezelfde DB + retentie via BackgroundService

Section titled “ADR-005 — Vluchtig én persistent in dezelfde DB + retentie via BackgroundService”

Context. Een deel van de data is vluchtig en anoniem (Vraag, Opinie, Opdracht, AnoniemeGebruiker, Sessie); een ander deel is persistent (Gebruiker, Bericht, Keuzelijst, Systeem). Een aparte datastore per categorie zou kosten en complexiteit toevoegen.

Beslissing. Vluchtige en persistente gegevens leven in dezelfde PostgreSQL-database. Vluchtige tabellen krijgen een timestamp-index en worden periodiek opgeschoond door een achtergrondtaak. Het opruimen verloopt via een BackgroundService (zie ADR-010) die verlopen Vragen en bijbehorende vluchtige data verwijdert, conform het Retentiebeleid.

Status. Aanvaard.

Gevolgen.

AspectGevolg
VoordeelEén datastore: lagere kosten en minder operationele overhead dan een aparte vluchtige store.
VoordeelTimestamp-indexen maken het periodiek opruimen goedkoop en voorspelbaar.
AnonimiteitVluchtige data verdwijnt structureel; het opruimen is eersteklas gedrag, geen bijzaak.
BeperkingVluchtige en persistente tabellen delen één DB-budget; indexen en opruiminterval moeten op het volume afgestemd blijven.
GevolgMediabytes vallen hier buiten: die staan in object-storage en worden door dezelfde retentie meegenomen (ADR-008/ADR-010).

Zie Anonimiteit & retentie.


ADR-006 — SignalR voor push, REST voor mutaties; Redis-backplane bij schaal

Section titled “ADR-006 — SignalR voor push, REST voor mutaties; Redis-backplane bij schaal”

Context. De matching en de opinie-cyclus hebben realtime updates nodig (een opdracht wordt toegewezen, voltooid of een vraag verloopt). Tegelijk moet alle statemutatie voorspelbaar en testbaar blijven, en mag realtime de horizontale schaal niet blokkeren.

Beslissing. Realtime push loopt via SignalR: een AanwezigheidHub (heartbeat/presence die laatsteBezoek/het online-venster voedt) en een BeoordelingHub (OpdrachtToegewezen, OpdrachtVoltooid, VraagVerlopen). Alle statemutaties gaan via REST; SignalR doet alleen push. Er is geen socket-state die scale-out blokkeert; bij meerdere instances komt er een Redis-backplane (een kwestie van configuratie, geen herontwerp).

Status. Aanvaard.

Gevolgen.

AspectGevolg
VoordeelHeldere scheiding: REST is de bron van waarheid voor mutaties; SignalR is puur notificatie. Goed testbaar.
VoordeelSchaal-klaar: zonder socket-state kunnen instances vrij bij- en afschalen; Redis verbindt ze pas wanneer nodig.
VerbeteringHet prototype had geen push (polling/niets); realtime maakt presence en de opinie-flow direct.
BeperkingTwee kanalen (REST + WebSocket) betekent iets meer infrastructuur dan louter REST; de winst in directheid weegt op.
ConfigEén instance draait zonder backplane; bij scale-out wordt Redis ingeschakeld via configuratie.

Zie Architectuur — Backend & API.


ADR-007 — Geen Keycloak; ASP.NET Core Identity + custom anonieme auth

Section titled “ADR-007 — Geen Keycloak; ASP.NET Core Identity + custom anonieme auth”

Context. I See You kent twee soorten gebruikers met heel verschillende eisen. Beheerders hebben een klassiek account; gebruikers moeten juist anoniem kunnen deelnemen — zonder verplichte persoonsgegevens, want elk gegeven dat niet nodig is ondermijnt de anonimiteit. Een geregistreerde gebruiker mag wél optioneel een e-mailadres opgeven, uitsluitend voor wachtwoordherstel. Een externe identity-server (zoals Keycloak) zou een extra component betekenen om te draaien, beveiligen en betalen, en past slecht bij anonieme accounts.

Beslissing. Geen Keycloak. Authenticatie is in-app:

  • Beheerders via ASP.NET Core Identity (gehashte wachtwoorden, MFA mogelijk, rol Beheerder).
  • Gebruikers via custom anonieme auth: een eigen Gebruiker/Sessie-aggregate, wachtwoorden met Argon2id, een fictieve naam, en standaard geen persoonsgegevens. Een geregistreerde gebruiker kan optioneel een email opgeven — uitsluitend voor wachtwoordherstel; zonder e-mail is er geen herstel. Een anonieme gebruiker heeft sowieso geen herstel. De gebruikerssessie is een single-device cookie-sessie (ADR-011).

Autorisatie verloopt via policies (zie ADR-013).

Status. Aanvaard.

Gevolgen.

AspectGevolg
Lage kostenGeen aparte identity-server om te hosten, te patchen en te beveiligen; alles draait in dezelfde API.
AnonimiteitGebruikers hebben standaard geen persoonsgegeven nodig; een e-mailadres is optioneel en wordt alleen voor wachtwoordherstel gebruikt.
BeveiligingArgon2id-hashing voor gebruikers; Identity verzorgt veilige hashing en optionele MFA voor beheerders.
BeperkingHerstel is alleen mogelijk met een opgegeven e-mailadres; zonder e-mail betekent een verloren fictieve naam/wachtwoord een verloren account — een bewuste prijs voor anonimiteit. Anonieme gebruikers hebben geen herstel.
GevolgTwee auth-mechanismen in één API; de scheiding zit in autorisatiebeleid, niet in aparte deployables.

Zie Beveiliging & privacy.


ADR-008 — IMediaStore: lokaal/MinIO ↔ S3-compatibel, cloud-agnostisch

Section titled “ADR-008 — IMediaStore: lokaal/MinIO ↔ S3-compatibel, cloud-agnostisch”

Context. Een Vraag bevat een mediabestand (video van maximaal 10 sec of foto). Mediabytes horen niet in de relationele database. De opslag moet goedkoop kunnen starten (lokaal/dev) en moeilijk cloud-lock-in opleveren bij opschalen.

Beslissing. Media wordt vastgelegd via live capture (POST /api/media/temp) en daarna gepromoveerd bij POST /api/vragen. De bytes gaan naar object-/blob-storage; de DB bewaart alleen de key + metadata. Een IMediaStore-abstractie maakt de opslag verwisselbaar: filesystem/MinIO lokaal, en één S3-compatibele implementatie (AWS SDK for .NET) die tegen elke S3-compatibele opslag werkt (AWS S3, GCS, MinIO, …). Azure Blob kan via die S3-laag of via een dunne AzureBlobMediaStore met dezelfde interface — wisselen = DI/config. Video wordt met range-streaming geserveerd.

Status. Aanvaard.

Gevolgen.

AspectGevolg
Lage kostenLokaal/dev draait op filesystem of MinIO zonder cloudkosten; productie gebruikt goedkope object-storage.
Cloud-agnostischEén S3-compatibele implementatie werkt bij meerdere providers; geen lock-in, wisselen via configuratie.
VoordeelMediabytes belasten de database niet; de DB blijft klein en goedkoop te schalen.
ImplementatieRange-streaming voor video; foto en video delen dezelfde opslag-pipeline (ADR-009).
RetentieVerlopen media wordt door de retentietaak verwijderd uit de store én de DB-metadata (ADR-010).

Zie Architectuur — Media & opname.


ADR-009 — Foto en video in één media-pipeline via MediaType

Section titled “ADR-009 — Foto en video in één media-pipeline via MediaType”

Context. Een vraag kan een video van maximaal 10 seconden (mag korter) of een foto zijn. De beoordeling (de door de maker gekozen secties, standaard Gezicht/Kleding/Accessoires) is identiek voor beide; alleen de capture verschilt. Twee aparte pijplijnen zouden duplicatie en inconsistentie opleveren.

Beslissing. Beide media generaliseren via de enum MediaType { Video, Foto } op de Vraag. Foto en video delen dezelfde pipeline — promotie, opslag (ADR-008), matching, opinie-cyclus en retentie. Alleen de capture verschilt: een opname van maximaal 10 seconden versus één frame. Er komt geen aparte Video- en Foto-tak in het model of de code.

Status. Aanvaard.

Gevolgen.

LaagGevolg
CaptureDe camera-hook (ADR-012) ondersteunt twee opname-modi via getUserMedia/MediaRecorder: video opnemen óf één foto vastleggen.
OpslagHet bestand kan video of afbeelding zijn; mediaType + key/metadata leggen vast welk type het is.
WeergaveDe opiniegever-UI toont video met afspeelbesturing of een statische foto, afhankelijk van mediaType; de beoordeelde secties zijn voor beide gelijk en volgen de Beoordelingsselectie van de maker.
ModelEén aggregate, één matching- en opinie-flow; minder duplicatie en consistente sectiegemiddelden.
RetentieFoto’s vallen onder hetzelfde retentiebeleid (ADR-010): altijd verwijderen zodra de beoordeling klaar is.

Zie het scenario Vraag stellen met foto en Media & opname.


Context. Anonimiteit staat of valt met het daadwerkelijk verwijderen van opnames. Het prototype kende een uitschakelbare opruimvlag (ENABLE_CLEANUP), waardoor media per ongeluk konden blijven bestaan. Voor I See You is dat onaanvaardbaar.

Beslissing. Retentie verloopt via een RetentieHostedService : BackgroundService die altijd actief is — er is geen uitschakelbare flag. De primaire trigger is gedragsmatig, niet op de klok: een Vraag wordt verwijderd zodra de beoordeling klaar is (alle opinies binnen). De achtergrondtaak ruimt daarnaast op: een via Beheer instelbare veiligheids-timeout (veiligheidsTimeoutMin) vangt vragen op die nooit afgerond raken, en het bereiken van de meldingen-drempel (standaard 3, instelbaar) verwijdert gemelde vragen. In alle gevallen worden zowel de mediabytes in de object-storage als de DB-metadata en bijbehorende vluchtige data verwijderd (ADR-005). De waarden komen uit de Systeeminstellingen en het Retentiebeleid.

Status. Aanvaard.

Gevolgen.

AspectGevolg
AnonimiteitConcrete, afdwingbare anonimiteit; opnames verdwijnen na afronding gegarandeerd — uiterlijk aan het einde van het resultaat-retentievenster (of eerder, wanneer de maker zelf verwijdert) — en kunnen niet “per ongeluk” blijven bestaan.
VerbeteringGeen ENABLE_CLEANUP-flag meer; opruimen is structureel en niet uit te zetten.
VoordeelBeperkt opslag en aansprakelijkheid; opruimen is eersteklas gedrag.
BeperkingVragen die nooit afgerond raken (te weinig of geen reagerende kandidaten) blijven niet hangen: de AI-persona (ADR-017) maakt de beoordeling af, en de veiligheids-timeout is het vangnet daarachter.
ImplementatieDe taak verwijdert media + DB-record + vluchtige bijhorende data in één opruimronde.

Zie Anonimiteit & retentie.


Context. I See You moet een eenduidig begrip van “online” hebben voor de matching; parallelle sessies vergroten het risico op manipulatie en complicaties rond anonimiteit. Het online-venster bepaalt of een gebruiker als online geldt en daarmee voorrang krijgt bij de matching — het is geen filter of harde voorwaarde.

Beslissing. Per gebruiker is er hooguit één actieve Sessie (single-device), als cookie-sessie. Een nieuwe login verdringt de vorige sessie. Verlopen sessies worden periodiek opgeruimd (ADR-005); laatsteBezoek (gevoed door de AanwezigheidHub, ADR-006) voedt het online-venster (een via Beheer instelbaar aantal minuten, standaard 10).

Status. Aanvaard.

Gevolgen.

AspectGevolg
AnonimiteitBeperkt parallel gebruik en daarmee misbruik-oppervlak.
VoordeelEenduidig online-begrip per gebruiker; eenvoudiger matching en minder dubbeltellingen.
BeperkingEen gebruiker kan niet gelijktijdig op twee apparaten ingelogd zijn; inloggen elders logt het eerste apparaat uit.
ImplementatiePer gebruiker wordt één actieve sessionId bewaard; bij login wordt een bestaande sessie ingetrokken.

Zie Beveiliging & privacy.


ADR-012 — React 19 + Vite + TypeScript als frontend

Section titled “ADR-012 — React 19 + Vite + TypeScript als frontend”

Context. De frontend draait om live camera-opname (video max. 10 sec / foto) in de browser via getUserMedia/MediaRecorder, met een toegankelijke, getypeerde UI en realtime updates. Alternatieven als Blazor, Angular, Vue en Svelte zijn afgewogen; doorslaggevend zijn camera-native webtoegang en de breedte van het ecosysteem (camera, formulieren, realtime, i18n).

Beslissing. De frontend is greenfield React 19 + Vite + TypeScript, in een monorepo (pnpm workspaces of Turborepo) met twee apps (beheerportaal, gebruikersportaal) + gedeelde libs (ui, api-client, shared). Verder: Tailwind CSS, toegankelijke componenten (Headless UI/Radix), React Router, TanStack Query (server-state), React Hook Form + Zod (formulieren/validatie), i18next (nl/en/es/fr), een uit OpenAPI gegenereerde getypeerde API-client, @microsoft/signalr-client en een getypeerde camera-hook voor getUserMedia/MediaRecorder (video max. 10 sec / foto). Mobiel-kader-layout.

Status. Aanvaard.

Gevolgen.

AspectGevolg
Camera-centrischDirecte, breed beschikbare toegang tot getUserMedia/MediaRecorder; de camera-hook is getypeerd en deelbaar.
VoordeelBreed ecosysteem dekt alle behoeften (camera, server-state, formulieren/validatie, i18n, realtime) met volwassen libs.
VoordeelTypeScript + een uit OpenAPI gegenereerde client geven end-to-end typeveiligheid tussen frontend en API.
VoordeelMonorepo deelt ui/api-client/shared tussen beide portalen zonder duplicatie.
BeperkingGeen volledige .NET-end-to-end (zoals Blazor zou geven); de OpenAPI-client overbrugt dat met gegenereerde types.

Zie Architectuur — Frontend.


ADR-013 — Twee portalen op één API via autorisatiebeleid

Section titled “ADR-013 — Twee portalen op één API via autorisatiebeleid”

Context. Er zijn twee portalen: een beheerportaal en een gebruikersportaal. Grofweg: beheer ≈ context Beheer + de admin-kant van Moderatie; gebruiker ≈ Beoordeling + Beloning & Financieel + melden + de gebruikerskant van Gebruiker & Toegang. Twee aparte API-deployables zouden de kosten en operationele last verdubbelen, terwijl de scheiding vooral over toegang gaat.

Beslissing. Twee frontends (ADR-012) draaien op één deployable API (de modulaire monoliet uit ADR-003). De scheiding tussen beheer en gebruiker verloopt via autorisatiebeleid (IsBeheerder vs. gebruiker/anoniem, ADR-007) — niet via een 1-op-1 context-grens of aparte API’s.

Status. Aanvaard.

Gevolgen.

AspectGevolg
Lage kostenEén API om te hosten, te bouwen en te bewaken in plaats van twee.
SchaalEén stateless deployable schaalt horizontaal; beide portalen profiteren van dezelfde scale-out (ADR-015).
VoordeelPolicies bepalen toegang fijnmazig; portaalgrenzen vallen niet samen met context-grenzen, wat flexibel is.
BeperkingDe autorisatiebeleids­logica moet zorgvuldig zijn: één API bedient zowel anonieme gebruikers als beheerders.
ConventieEndpoints krijgen expliciet een policy; standaard is dichtgezet (deny-by-default).

Zie Architectuur — Backend & API en Frontend.


Context. I See You bedient één logische omgeving/community; er is geen behoefte aan meerdere geïsoleerde klanten binnen dezelfde installatie. Multi-tenancy zou data-isolatie, tenant-routing en extra schema-complexiteit toevoegen zonder dat een eis daarom vraagt.

Beslissing. De applicatie is single-tenant: één database, één set tabellen, geen tenant-discriminator of per-tenant isolatie.

Status. Aanvaard.

Gevolgen.

AspectGevolg
Lage kostenGeen multi-tenant-complexiteit (tenant-kolommen, row-level isolatie, routing); eenvoudiger te bouwen en te bewaken.
VoordeelEenvoudiger datamodel en query’s; minder kans op tenant-lek-fouten.
BeperkingEen tweede, geïsoleerde omgeving zou een aparte deployment/DB vereisen.
HerzieningMocht multi-tenancy ooit nodig worden, dan vraagt dat een nieuwe ADR en een schemawijziging.

Zie Architectuur.


ADR-015 — Cloud-agnostische hosting met scale-to-zero

Section titled “ADR-015 — Cloud-agnostische hosting met scale-to-zero”

Context. I See You moet goedkoop kunnen starten (weinig verkeer in het begin) maar klaar zijn voor pieken in opinie-activiteit, zonder vast te zitten aan één cloudleverancier. De backend is bewust stateless (ADR-003/ADR-006) om dit mogelijk te maken.

Beslissing. De API draait als Docker-image op een serverless-container-platform met scale-to-zero (lage kosten bij weinig verkeer) en autoscaling (klaar voor pieken). Daarnaast: managed PostgreSQL (ADR-002), S3-compatibele object-storage (ADR-008) en bij opschalen Redis als SignalR-backplane (ADR-006). De keuze is cloud-agnostisch (voorbeelden als Cloud Run / App Runner-Fargate / Container Apps zijn noembaar maar neutraal); geen lock-in. Lokale dev draait via Docker Compose (optioneel .NET Aspire).

Status. Aanvaard.

Gevolgen.

AspectGevolg
Lage kostenScale-to-zero betekent (bijna) geen compute-kosten bij weinig verkeer.
SchaalAutoscaling vangt pieken op; de stateless API schaalt horizontaal, met Redis-backplane voor realtime.
Cloud-agnostischEén Docker-image draait op meerdere platforms; managed Postgres + S3-compatibele storage vermijden lock-in.
BeperkingScale-to-zero kan een koude-start geven na inactiviteit; acceptabel voor het verwachte gebruikspatroon.
DevDocker Compose biedt een lokale omgeving (API + Postgres + MinIO) die de productieopzet benadert.

Zie Architectuur — Deployment.


ADR-016 — Land als keuzelijst (ISO 3166-1) + afgeleide UN M49-regio (vervangen door ADR-037)

Section titled “ADR-016 — Land als keuzelijst (ISO 3166-1) + afgeleide UN M49-regio (vervangen door ADR-037)”

Vervangen (2026-07-11m). Deze ADR is ingetrokken door ADR-037: het Land-veld en de hier beschreven UN M49-verbreding zijn geschrapt. De tekst hieronder blijft staan als historische context.

Context. Het prototype bepaalde locatie via een vrij nationaliteit-veld en een handmatige NL-regiolijst (Noord/Zuid/Midden/Oost/West). Dat schaalt niet internationaal: een vrije tekstwaarde is niet vergelijkbaar voor matching, en een vaste Nederlandse regio-indeling werkt niet voor andere landen. I See You moet voor álle landen werken, met minimale invoer voor de gebruiker en met respect voor anonimiteit (geen onnodige persoonsgegevens, geen locatie-tracking). Een eerdere variant van deze ADR leidde het land automatisch via GeoIP uit het IP-adres af; dat is verlaten ten gunste van een eenvoudige keuze door de gebruiker.

Beslissing. Locatie is gestandaardiseerd, maar door de gebruiker gekozen:

  • Land volgens ISO 3166-1 (code alpha-2 + gelokaliseerde naam) is het primaire locatieveld. De gebruiker kiest het land uit een Keuzelijst (gevuld met de ISO 3166-1-landen, per taal). Er is geen GeoIP, geen automatische detectie, geen permissieprompt en geen vooringevulde waarde op basis van het IP.
  • RegioM49 (Geografische regio) volgens UN M49 (werelddeel + subregio) wordt automatisch afgeleid uit het gekozen land; de gebruiker vult dit niet zelf in.
  • Er is geen locatie-tracking: het IP wordt niet voor locatie gebruikt of bewaard (het bestaat al voor rate-limiting), en er wordt géén Geolocation-API / GPS-prompt gebruikt. Het land is gewoon een keuze.
  • De landenlijst is beheerbare stamdata in de Keuzelijst (naast gender en culturele achtergrond); de afgeleide UN M49-regio is dat niet, die komt uit standaard referentiedata.

De SelectieCriteria matchen op het exacte land en verbreden via de afgeleide UN M49-regio (land → subregio → werelddeel) wanneer er te weinig kandidaten in exact hetzelfde land zijn.

Status. Aanvaard (vervangt de GeoIP-locatievariant).

Gevolgen.

AspectGevolg
SchaalWerkt voor álle landen via wereldwijde standaarden; geen handmatige per-land regiolijst meer te onderhouden.
VoordeelEén bewuste keuze uit een gelokaliseerde landenlijst; matching kan verbreden van land → subregio → werelddeel via de afgeleide UN M49-regio.
AnonimiteitGeen locatie-tracking nodig: het land is gewoon een keuze, er is geen GeoIP- of GPS-bepaling en het IP wordt niet voor locatie bewaard.
Lage kostenGeen GeoIP-dataset/-library te onderhouden of te betalen; de landenlijst is reguliere stamdata in de Keuzelijst.
BeperkingDe gebruiker moet het land zelf kiezen (één extra keuze) in plaats van het automatisch te krijgen; dat weegt op tegen het vermijden van locatie-tracking.
ImplementatieLand komt uit de Keuzelijst (ISO 3166-1); de UN M49-regio wordt eruit afgeleid; gender en culturele achtergrond blijven keuzes.

Zie Beveiliging & privacy en het Domeinmodel — Gebruiker & Toegang.


ADR-017 — Credits als deelname-poort, instelbare parameters, online als voorrang en AI-persona-vangnet

Section titled “ADR-017 — Credits als deelname-poort, instelbare parameters, online als voorrang en AI-persona-vangnet”

Context. Het prototype werkte met vaste getallen: 10 opiniegevers per vraag, 30 minuten levensduur, 3 meldingen, een vast online-venster. In de praktijk hangen die waarden af van het verkeer en de community-omvang; bij weinig actieve gebruikers leiden harde grenzen tot vragen die nooit een (volledige) beoordeling krijgen. Tegelijk moet de matching robuust zijn en moet de maker grip houden op wat er beoordeeld wordt en wie vrije tekst mag achterlaten. Ten slotte heeft I See You een deelname-poort nodig die het stellen van vragen koppelt aan het léveren van opinies. Eerdere modellen kenden daarvoor twee overlappende poorten — een vast aantal vragen dat je eerst zelf moest beoordelen (aantalEerstZelfBeoordelen) én een reciprociteits­drempel die een vraag-slot eerst Geblokkeerd hield (reciprociteitDrempel/Vraag.reciprociteitDoel). Twee poorten voor één doel zijn verwarrend en houden slots half in de pool.

Beslissing. Het beoordelingsproces wordt configureerbaar en zelfherstellend, met credits als de enige deelname-poort:

  • Credits als deelname-poort (vervangt beide eerdere poorten). Credits zijn de deelname-valuta: een gebruiker verdient creditsPerBeoordeling (standaard 5) per voltooide menselijke opinie en betaalt de op de Vraag bevroren inzage-prijs (= aantalBeoordelingen × creditkostenPerBeoordeling, standaard 1; het aantal is de bij het stellen bevroren instelling aantalOpiniegevers — zie ADR-022) om zijn eigen uitslag in te zien. Een Vraag stellen is gratis — de poort staat op het inzien van het resultaat: is de vraag beoordeeld, dan ontgrendelt de maker de uitslag door eenmalig de credits af te schrijven; heeft hij te weinig, dan moet hij eerst verdienen (beoordelen). Het saldo is zichtbaar; bij een klare uitslag worden de inzage-kosten + het saldo vooraf getoond. Dit vervangt zowel aantalEerstZelfBeoordelen als reciprociteitDrempel: beide zijn verwijderd, en daarmee vervallen ook de Geblokkeerd-OpdrachtStatus en Vraag.reciprociteitDoel. Slots ontstaan voortaan altijd Open. Een Gebruiker houdt een persistent saldo (start 0 bij registratie); een AnoniemeGebruiker heeft een sessie-gebonden saldo dat met de sessie vervalt (anoniemeSessieMin). Zie de functionele Credits en het scenario Credits verdienen en besteden.

    Herzien (2026-07-02): poort verplaatst van insturenuitslag inzien. Beprijzen bij het insturen veroorzaakte een cold-start deadlock (iedereen start op 0; niemand kan posten, dus er is niets te beoordelen, dus niemand verdient). Omdat een gestelde vraag juist werk/verdien-kans voor anderen is en beoordeel-inspanning het schaarse goed is, is posten nu gratis en betaal je om je uitslag in te zien. Dit bootstrapt de economie vanzelf en is de credit-vorm van de oorspronkelijke wederkerigheid (“beoordeel een ander om je eigen antwoord te krijgen”).

  • Instelbare parameters via Systeeminstellingen. Het aantal beoordelingen per vraag (aantalOpiniegevers), de antwoordtermijn (antwoordtermijnMin), de creditkosten per beoordeling (creditkostenPerBeoordeling, standaard 1) en de credits per beoordeling (creditsPerBeoordeling, standaard 5), de anonieme-sessieduur (anoniemeSessieMin), het online-venster (onlineVensterMin), de veiligheids-timeout (veiligheidsTimeoutMin) en de meldingen-drempel (meldingenDrempel) zijn via Beheer instelbaar. Er staan geen harde getallen meer in het model; standaardwaarden (bv. creditkosten per beoordeling standaard 1, credits per beoordeling standaard 5, meldingen-drempel standaard 3, online-venster standaard 10 min) zijn alleen vertrekpunten.

  • Online = voorrang, geen filter. Online-zijn is geen SelectieCriteria en geen keuze van de vrager. Online kandidaten krijgen voorrang bij de matching, maar offline kandidaten worden niet uitgesloten.

  • AI-persona-vangnet. Reageert een opiniegever niet binnen de antwoordtermijn, dan valt de Opdracht terug naar Open en wordt het slot opnieuw beschikbaar. Is er geen mens beschikbaar (te weinig online of niemand reageert), dan beoordeelt een AI-persona (Opinie.bron = AI), zodat elke vraag een complete beoordeling krijgt. De concrete AI-invulling was hier nog een open ontwerppunt (inmiddels ingevuld in ADR-024).

  • Beoordelingsselectie door de vrager. Na de opname kiest de maker welke secties/attributen beoordeeld worden (Beoordelingsselectie); de opiniegever scoort alleen die. De beschikbare secties/attributen zijn beheerbare stamdata (Keuzelijst); Gezicht/Kleding/Accessoires zijn nog slechts de standaard, geen drie vaste secties.

  • Vrije tekst voor iedereen, mits door de moderatie. Er is geen sterhouder-poort meer op vrije tekst: iedere opiniegever mag één vrije reactie achterlaten (optioneel). Alle vrije tekst — de toelichting van de maker én de reactie van de reviewer — loopt vooraf langs de tekstmoderatie. Afgekeurde tekst wordt geweigerd, niet opgeslagen, en levert géén credit op.

Status. Aanvaard.

Gevolgen.

AspectGevolg
DeelnameEén heldere quid-pro-quo: je uitslag inzien kost credits, beoordelen verdient ze; stellen is gratis. De poort (op het inzien) vervangt twee overlappende poorten en is voor de gebruiker eenvoudig te begrijpen (saldo + kosten zichtbaar), en voorkomt de cold-start deadlock.
VereenvoudigingMet het schrappen van Geblokkeerd en reciprociteitDoel ontstaan slots altijd Open; geen half-in-de-pool hangende slots, en de Opdracht-toestandsmachine wordt eenvoudiger.
SchaalHet platform past zich aan het verkeer aan: aantallen, termijnen en credit-bedragen worden afgestemd zonder code- of modelwijziging.
RobuustheidRandgevallen “te weinig of geen kandidaten” worden door de AI-persona en het opnieuw toewijzen opgevangen, in plaats van met minder of geen opinies.
AnonimiteitDe maker bepaalt zelf wat beoordeeld wordt; vrije tekst staat voor iedereen open, maar loopt langs de tekstmoderatie, wat de drempel voor misbruik verhoogt. Anonieme credits vervallen met de sessie, zodat er geen blijvend, herleidbaar saldo achterblijft.
VoordeelOnline-voorrang houdt de matching snel zonder offline kandidaten hard uit te sluiten.
BeperkingInstelbare parameters (incl. credit-bedragen) vragen om verstandige standaardwaarden en bewaking; de AI-persona-invulling moet nog uitgewerkt worden.
ImplementatieDe Opdracht-flow drijft het opnieuw toewijzen; de Systeeminstellingen voeden matching, antwoordtermijn, credit-bedragen en drempels. Het credit-saldo wordt bij een voltooide menselijke opinie bijgeschreven en bij het inzien van de eigen uitslag afgeschreven.

Zie het scenario Matching van opiniegevers en het Domeinmodel — Beoordeling.


ADR-018 — Credits ≠ sterren (gescheiden valuta)

Section titled “ADR-018 — Credits ≠ sterren (gescheiden valuta)”

Ingetrokken (2026-07-03). Deze ADR ging uit van sterren als loterij-beloning met privileges en uitbetaling — een model dat volledig is geschrapt. De sterhouder-poort op vrije tekst is vervallen. De onderstaande tekst is historisch en niet meer leidend; zie ADR-019. (Let op — per 2026-07-05, ter verificatie: de ster keert terug als verdiende reputatie — géén loterij — en er komt weer geld in het systeem via ADR-027ADR-029; deze loterij-ADR blijft desondanks ingetrokken.)

Context (historisch). De credits-poort (ADR-017) introduceert een deelname-valuta. Het toen geldende model kende daarnaast een schaarse beloning in de vorm van sterren. Omdat je beide door te beoordelen kon opbouwen, lag verwarring op de loer — alsof credits en die beloning hetzelfde waren of in elkaar omgerekend konden worden. Het model moest beide expliciet gescheiden houden.

Beslissing (ingetrokken). Credits en sterren zijn twee aparte valuta met verschillende doelen, en worden niet samengevoegd of in elkaar omgerekend:

  • Credits = deelname. Je verdient er één per élke voltooide menselijke opinie (creditsPerBeoordeling) en betaalt ermee om je eigen uitslag in te zien; stellen is gratis.
  • Sterren = beloning + privileges. Een ster is een loterij-beloning: per Vraag krijgt hooguit één opiniegever er soms één. Het Sterrensaldo ontgrendelt bij een drempel privileges — vrije opmerkingen bij een drempel — en leidt bij de uitbetaaldrempel tot een Uitbetaling.

Een AI-persona-opinie maakt een vraag wél compleet, maar levert noch credits noch een ster op.

Status. Ingetrokken (sterren-als-beloning + uitbetaling geschrapt; zie ADR-019).

Gevolgen (vervallen). De onderstaande tabel beschrijft het ingetrokken model en is niet meer leidend:

AspectGevolg (vervallen)
HelderheidTwee doelen gescheiden: deelname (credits) versus beloning/privileges (sterren).
AnonimiteitVrije opmerkingen gebonden aan sterhouders (een privilege).
VoordeelDe ster als schaarse loterij-beloning (één per vraag).
BeperkingTwee saldi naast elkaar (credits + sterren) vragen om duidelijke UI-communicatie.
Implementatiecredits en Sterrensaldo zijn aparte velden; sterren behouden hun toekenning en uitbetaling.

Zie voor het geldende model de functionele Credits en Waardering & reviews, en het scenario Opinie geven & waardering.


Section titled “ADR-023 — Uitslagnotificatie per e-mail: alleen een link, via Azure Communication Services”

Context. Een maker legt een Vraag voor en moet daarna terugkomen om de uitslag te bekijken. Zonder seintje betekent dat handmatig blijven checken of de beoordeling al klaar is. De stakeholder gaf aan (review 2-7-2026, Han): “een mailtje met een link is prima — dan bezoeken ze ook de site opnieuw.” Tegelijk mag zo’n notificatie de anonimiteit en dataminimalisatie niet ondergraven: geen gevoelige beoordelingsinhoud in een (minder beveiligd) e-mailkanaal, en geen blijvende mailinglijst.

Beslissing. I See You stuurt de maker één e-mailnotificatie zodra de uitslag klaar is (bij het zetten van beoordeeldOp), met alleen een link naar het gebruikersportaal:

  • Optioneel veld op de vraag. Vraag krijgt een nullable notificatieEmail, dat de maker bij het stellen kan achterlaten. Het staat volledig los van het optionele account-e-mailadres voor wachtwoordherstel op de Gebruiker; ook een anonieme maker kan het opgeven. Het API-veld NotificatieEmail bij POST /api/vragen krijgt een simpele formaatvalidatie.
  • Alleen een link, geen inhoud. De mail bevat geen scores, gemiddelden of reacties — alleen een link (“je uitslag staat klaar”). De site is de bron (de uitslag zit achter de credits-poort); dit is robuust tegen aflever-/spamproblemen.
  • Poort + ACS-implementatie. De applicatie-poort IUitslagNotificaties (UitslagKlaarAsync(email)) wordt geïmplementeerd op Azure Communication Services (ACS) Email (afzender DoNotReply@…azurecomm.net), via de app-settings Email__ConnectionString, Email__Afzender en App__PortaalUrl.
  • Fail-graceful. Zonder configuratie of bij een verzendfout blijft de app werken: de uitslag is altijd in de app te zien, de mail is best-effort en blokkeert het afronden van de vraag niet.
  • Adres eenmalig. Direct na de verzendpoging wordt notificatieEmail op null gezet (of de verzending nu slaagde of niet), zodat er geen adres achterblijft; het zou hoe dan ook met de vraag verdwijnen (retentie).

Status. Aanvaard (2026-07-04e).

Gevolgen.

AspectGevolg
GebruikswaardeDe maker hoeft niet zelf te blijven checken; hij krijgt een seintje en keert via de link terug naar het portaal.
AnonimiteitGeen beoordelingsinhoud in e-mail; het adres wordt eenmalig gebruikt en direct gewist — geen mailinglijst, geen herleidbaar spoor. Los van het herstel-adres.
Lage kostenACS Email is een lichte, pay-per-use dienst; geen eigen mailserver te beheren.
RobuustheidFail-graceful: aflever-/configuratieproblemen raken de kernfunctionaliteit niet; de uitslag blijft in de app leidend.
BeperkingEén afzenderdomein/-configuratie (ACS) nodig in productie; zonder configuratie vervalt de notificatie stilzwijgend (no-op).
ImplementatieDe poort wordt aangeroepen in dezelfde flow die beoordeeldOp zet; het adres wordt in die transactie gewist. Het API-veld NotificatieEmail bij POST /api/vragen wordt licht gevalideerd.

Zie Backend & API — Uitslagnotificatie, Beveiliging & privacy en de term Uitslagnotificatie.


ADR-024 — Vision-gebaseerde AI-persona op gpt-4o-mini + handmatige AI-opinie

Section titled “ADR-024 — Vision-gebaseerde AI-persona op gpt-4o-mini + handmatige AI-opinie”

Context. Het AI-persona-vangnet (ADR-017) garandeert dat elke Vraag opinies krijgt, maar de concrete AI-invulling was tot nu een open punt: de AIPersona leverde alleen een deterministische (regelgebaseerde) score, blind voor de opname. Dat is weinig geloofwaardig — een opinie over uiterlijk/kleding/accessoires hoort op de beeldinhoud te slaan. Tegelijk was er alleen een automatisch vangnet (ná de maxOnbeantwoordMin-deadline); de beheerder had geen manier om een trage of vastgelopen vraag direct af te vullen. Twee wensen dus: een echte, op de opname gebaseerde AI-beoordeling, en een handmatige beheer-actie — zonder de kosten, de anonimiteit of de robuustheid te schaden.

Beslissing. De AI-persona wordt een echte, vision-gebaseerde dienst, en de beheerder krijgt een handmatige trigger:

  • IAiPersonaService op OpenAI gpt-4o-mini (vision). De opinie-generatie loopt via de applicatie-poort IAiPersonaService, met een standaard-adapter op OpenAI gpt-4o-mini (een goedkoop vision-model). De persona-instructie stuurt de “persoonlijkheid”; het model scoort alleen de door de maker gekozen secties/attributen — destijds op de schaal 1–5; per 2026-07-05 is dat de verplichte 1–5-schaal. De AI produceerde in deze versie geen vrije tekst (dat is met ADR-025 gewijzigd).
  • Foto direct, video via FFmpeg-frames. Een foto gaat rechtstreeks als beeld mee. Bij een video (max. 10s) haalt de backend met FFmpeg ~4 frames uit de opname en stuurt die als beelden mee. FFmpeg zit in het API-Docker-image.
  • Handmatige AI-opinie via het beheerportaal. Naast het automatische vangnet kan de beheerder met POST /api/beheer/vragen/{id}/ai-opinie (body { personaId }) direct één AI-opinie op een Open slot laten genereren met een gekozen persona. 404/400 als er geen open slot is. Vult alleen Open slots — nooit een door een mens vastgehouden slot.
  • Eén sleutel, extra permissie. De adapter gebruikt dezelfde app-setting OpenAI__ApiKey als de tekstmoderatie, maar heeft daarnaast de chat/completions-permissie (“Model capabilities: write”) nodig.
  • Fail-graceful. Ontbreekt de sleutel of de chat-permissie, of faalt de aanroep, dan valt de dienst terug op de bestaande deterministische scores, zodat er altijd een opinie ontstaat en de app blijft werken. Kosten: ordegrootte < $0,01 per AI-opinie.

Status. Aanvaard (2026-07-04f). Werkt ADR-017/ADR-019 uit (het “open ontwerppunt” van de AI-invulling is nu ingevuld).

Gevolgen.

AspectGevolg
GeloofwaardigheidDe AI-opinie slaat nu op de werkelijke opname (vision), niet op een blinde regel; het vangnet levert relevantere scores.
BestuurbaarheidDe beheerder kan een trage/vastgelopen vraag direct afvullen zonder op de deadline te wachten, met een zelfgekozen persona.
Lage kostengpt-4o-mini is een goedkoop vision-model; met 1 beeld (foto) of ~4 frames (video) blijft een AI-opinie onder ~$0,01.
RobuustheidFail-graceful terugval op deterministische scores: zonder sleutel/permissie of bij een fout werkt de app en het vangnet gewoon door.
AnonimiteitDe AI produceert geen vrije tekst en er blijft geen apart frame-archief achter (frames zijn vluchtig, in-memory); de video zelf volgt het gewone retentiebeleid.
BeperkingVereist FFmpeg in het API-image en een OpenAI-sleutel met chat/completions-permissie in productie; zonder die permissie draait alleen de deterministische terugval.
ImplementatieHandmatige actie via POST /api/beheer/vragen/{id}/ai-opinie (beheerder-only); dezelfde IAiPersonaService dient het automatische én het handmatige pad.

Zie Backend & API — AI-persona-vangnet, Media & opname — Frame-extractie, Systeembeheer en de termen AI-persona/AI-opinie.


ADR-025 — Maker bepaalt de vrije reactie (verplicht wanneer gevraagd); AI geeft nu een reactie

Section titled “ADR-025 — Maker bepaalt de vrije reactie (verplicht wanneer gevraagd); AI geeft nu een reactie”

Herzien (2026-07-10h, issue #17). Het maker-keuze-deel is vervallen: de vlag VrijeTekstGevraagd bestaat functioneel niet meer. Vrije (tekst)reacties komen voortaan uit vrije-reactie-slots (Opdracht.minimumNiveau ≥ Niveau1) die exclusief door beoordelaars met een adviesniveau worden gevuld — max. maxVrijeReactiesZonderBetaald per vraag zonder betaald advies; mét betaald advies zijn de niveaueisen leidend. Zie ontwerpbeslissing 34. Het AI-reactie-deel (AI krijgt de toelichting en kan een reactie leveren) blijft gelden.

Context. De vrije reactie van de opiniegever was altijd optioneel (ADR-021), en een Vraag moest minstens één sectie bevatten. Een maker die vooral (of uitsluitend) een geschreven reactie wil, kon dat niet afdwingen; een puur-tekstuele vraag zonder smiley-scores kon niet. Tegelijk leverde de AI-persona (ADR-024) geen vrije tekst — waardeloos voor een alleen-vrije-tekst-vraag en weinig behulpzaam elders — en kreeg de prompt de Vraag.toelichting niet mee. Beide punten samen (besloten met de gebruiker, 3-7-2026).

Beslissing. Twee samenhangende wijzigingen:

  • Maker bepaalt de vrije reactie; verplicht wanneer gevraagd. Beoordelingsselectie krijgt de vlag VrijeTekstGevraagd (bool). Een vraag is geldig zodra er ≥1 sectie is OF VrijeTekstGevraagd = true — dus alleen categorieën, alleen een vrije reactie, of beide. Is VrijeTekstGevraagd = true, dan is de Opinie.reactie voor de opiniegever verplicht (een lege reactie wordt geweigerd, geen credit); is die false, dan wordt er geen reactieveld gevraagd. De reactie blijft door de tekstmoderatie lopen. POST /api/vragen krijgt VrijeTekstGevraagd en Selectie mag leeg zijn; POST /api/opdrachten/{id}/opinie weigert een lege verplichte reactie.
  • AI-persona geeft nu een onderbouwde reactie + krijgt de toelichting. De poort IAiPersonaService krijgt nu óók de Vraag.toelichting mee en geeft scores + een korte vrije reactie terug (niet meer alleen scores). De prompt is herkaderd: expliciet dat de persoon zélf, vrijwillig (opt-in), om opbouwende stijl-/uiterlijkfeedback vroeg, met de vraag om constructieve feedback in plaats van een “aantrekkelijkheidsoordeel”. De AI-reactie hoeft niet door de tekstmoderatie (ze komt van het model). De deterministische fallback geeft een generieke reactie.

Status. Aanvaard (2026-07-04g). Werkt ADR-021 (reactie nu verplicht-wanneer-gevraagd) en ADR-024 (AI levert nu een reactie) verder uit.

Gevolgen.

AspectGevolg
GebruikswaardeDe maker stuurt zelf of hij tekst wil; een alleen-vrije-tekst-vraag (zonder smiley-scores) is nu mogelijk, en een gevraagde reactie komt gegarandeerd binnen.
GeloofwaardigheidDe AI beantwoordt nu óók alleen-vrije-tekst-vragen en geeft elders een echte, onderbouwde reactie i.p.v. “geen reactie”; met de toelichting erbij is de feedback gerichter.
ContentbeleidDe herkaderde, opt-in prompt verhoogt de bereidheid van het model om constructieve feedback over echte mensen te geven — geen 100%-garantie; bij terughoudendheid is een ander model de vervolgstap.
RobuustheidDe verplicht-regel zit in de use-case (niet als DB-constraint); de fallback levert alsnog een deterministische opinie met generieke reactie.
BeperkingDe AI-reactie wordt bewust niet gemodereerd (modeltekst); dat leunt op de prompt-kadering en het model-contentbeleid.
ImplementatieBeoordelingsselectie.VrijeTekstGevraagd (bool); API-velden VrijeTekstGevraagd + optionele Selectie bij POST /api/vragen; verplicht-check bij POST /api/opdrachten/{id}/opinie; IAiPersonaService geeft AiBeoordeling(Scores, Reactie?) en krijgt de toelichting.

Zie Backend & API, Vraag stellen, Opinie geven en de termen Beoordelingsselectie/Reactie.


ADR-026 — Standaard 1 beoordeling; AI-herkomst verborgen voor de maker; elke beoordeling waardeerbaar

Section titled “ADR-026 — Standaard 1 beoordeling; AI-herkomst verborgen voor de maker; elke beoordeling waardeerbaar”

Context. Het aantal beoordelingen per vraag is een vaste beheerinstelling (aantalOpiniegevers, op de vraag bevroren als aantalBeoordelingenADR-022); de standaardwaarde stond op 10. In de praktijk is één beoordeling vaak genoeg voor een snelle indruk en houdt een lage default de eerste ervaring (en de AI-vangnet-kosten) klein. Daarnaast toonde de user-facing uitslag per opinie de herkomst (mens/AI): de maker zag dat een beoordeling van een AI-persona kwam, en de eerdere regel was dat een AI-opinie niet gewaardeerd kon worden. Dat is inconsistent — als de maker de herkomst tóch niet hoort te zien, moet hij ook elke ontvangen beoordeling kunnen waarderen, ongeacht de bron (besloten met de gebruiker, 3-7-2026).

Beslissing. Drie samenhangende wijzigingen:

  • Standaard aantalOpiniegevers = 1 (was 10). De standaardwaarde van de beheerinstelling aantalOpiniegevers gaat van 10 naar 1; ze blijft instelbaar via de Systeeminstellingen. Dit raakt alleen de default, niet het mechanisme uit ADR-022. (Dit default-deel is per 2026-07-05 teruggedraaid naar 10 — zie ADR-030.)
  • AI-herkomst verborgen voor de maker. Het veld bron (mens/AI) is verwijderd uit de user-facing uitslag-respons GET /api/vragen/{id}/uitslag: de maker ziet de herkomst van een opinie niet meer. Het datamodel houdt Opinie.bron (de enum OpinieBron) onveranderd bij; de beheer-endpoints tonen de herkomst nog wél (zie GET /api/beheer/vragen).
  • Elke ontvangen beoordeling is waardeerbaar (0–5), óók een AI-opinie. De eerdere regel “AI-opinies worden niet gewaardeerd” vervalt. Het endpoint POST /api/opinies/{id}/waardering werkt op elke opinie; een waardering op een AI-opinie wordt wél opgeslagen maar beïnvloedt geen reviewer-profiel (er is geen reviewer, dus de reviewer-statistieken op de Gebruiker blijven onaangeroerd). Een waardering op een menselijke opinie stelt de reviewer-totalen zoals voorheen bij.

Status. Aanvaard (2026-07-04h). Werkt ADR-017/ADR-022 (default) en ADR-019/ADR-024 (waardering + AI-herkomst) verder uit.

Gevolgen.

AspectGevolg
GebruikswaardeEen lage default (aantalOpiniegevers = 1) maakt een vraag snel compleet en de eerste ervaring eenvoudig; wie meer beoordelingen wil, verhoogt de instelling.
ConsistentieDe maker ziet geen herkomst meer én kan elke ontvangen beoordeling waarderen — als je AI niet kunt onderscheiden, moet je alles kunnen waarderen.
Zuivere statistiekenEen waardering op een AI-opinie raakt geen reviewer-profiel; de reviewer-statistieken blijven zuiver (alleen menselijke opinies tellen mee).
Anonimiteit/privacyDe maker krijgt geen signaal over de bron van zijn beoordelingen; de herkomst blijft intern (datamodel + beheer).
BeperkingBij aantalOpiniegevers = 1 leunt een beoordeling sterker op die ene opinie (mens of AI-vangnet); de beheerder kan de default verhogen wanneer meer opinies gewenst zijn.
ImplementatieDefault aantalOpiniegevers = 1 in de Systeeminstellingen; bron weggelaten uit de uitslag-DTO (behouden in beheer-DTO’s); de waardering-use-case laat de reviewer-update weg bij een AI-opinie.

Zie Backend & API, Resultaat terugzien en de termen OpinieBron/Waardering.


ADR-027 — Sterren als verdiende kwaliteitsreputatie via de consensus-ranking

Section titled “ADR-027 — Sterren als verdiende kwaliteitsreputatie via de consensus-ranking”

Geverifieerd door Han (2026-07-08); parameters vastgesteld in 2026-07-08b en gebouwd. Gebaseerd op de handgeschreven notities van 3-7-2026, die eerdere beslissingen overrulen; zie het voorstel-document voor de resterende ⚠️-interpretaties. De sterren + consensus-ranking zijn in de code geïmplementeerd (2026-07-08).

Context. In 2026-07-03 zijn de loterij-Ster en de uitbetaling geschrapt (ADR-019); alleen de 0–5-waardering bleef. De stakeholder-notities van 3-7-2026 (“Wanneer krijg je een ster”) herintroduceren de ster — niet als loterij maar als verdiende kwaliteitsreputatie, met expliciete toekenningsregels. Kwaliteit moet zichtbaar en blijvend beloond worden, en die reputatie moet (via ADR-028) iets waard zijn.

Beslissing. De Ster keert terug in een geherintroduceerde context Beloning & Financieel, met vier verdien-routes:

  • Beste opiniegever van een vraag — bepaald door de consensus-ranking: per afgeronde vraag een ranglijst op basis van consensus (afstand tot het gewogen groepsgemiddelde) + correlatie-controle (consistentie over de attributen), met de maker-waardering (0–5) als gewogen factor (gewichtMakerWaardering, ook 0 mogelijk). Er wordt pas geranked vanaf minBeoordelaarsVoorRanking (standaard 3) menselijke beoordelaars (interpretatie); AI-opinies dingen niet mee. De exacte formule is een implementatiekeuze.
  • 5× als 2e geëindigd (sterDrempelTweedePlaatsen), 10× als 3e geëindigd (sterDrempelDerdePlaatsen) en elke 200 verdiende credits (sterCreditsMijlpaal; elk veelvoud — interpretatie).
  • Alle drempels en gewichten zijn beheerinstellingen; sterren worden blijvend opgeslagen (Gebruiker.aantalSterren; of ze ooit vervallen is een open vraag).

Status. Voorgesteld (2026-07-05, ter verificatie). Overrulet het “geen sterren”-deel van ADR-019; de loterij (ADR-018) blijft geschrapt.

Gevolgen.

AspectGevolg
KwaliteitGoede beoordelaars worden zichtbaar en blijvend beloond; de ranking straft willekeurig/extreem scoren af (correlatie-controle).
SamenhangDe bestaande waardering krijgt een tweede functie (wegingsfactor) zonder te veranderen voor de maker.
BestuurbaarheidDrempels/gewichten via Beheer; de formule zelf is een implementatiekeuze na verificatie.
BeperkingRanking vereist ≥ 3 beoordelaars; bij kleinere vragen loopt alleen de credits-mijlpaal-route. Gelijkspel-afhandeling is een implementatiekeuze.
ImplementatieServer-side berekening in de afrond-flow van een vraag; Ster-records + tellers op de Gebruiker.

Zie Waardering & reviews, het scenario Een ster verdienen en het domeinmodel.


ADR-028 — Betaald advies met adviesniveaus en de teaser-flow

Section titled “ADR-028 — Betaald advies met adviesniveaus en de teaser-flow”

Geverifieerd door Han (2026-07-08); parameters vastgesteld in 2026-07-08b. De vraagsteller kiest per niveau een aantal beoordelaars; de vraagsteller koopt hoogstens één teaser en de beoordelaar wordt alleen bij aankoop betaald; de verdeelsleutel is 50/50; het niveau volgt uit vaste sterrendrempels (30/60/90/120★, ⚠️ Expert 150★). De code is bijgewerkt (2026-07-08). Zie het voorstel-document, §2–§3.

Context. Tekstadvies van bewezen goede beoordelaars is geld waard, maar het systeem kende geen manier om ervoor te betalen of eraan te verdienen. Tegelijk moet de gratis route (smiley-scores + de door de maker gevraagde gratis reactie) intact blijven, en moet ook een vraagsteller die niets instelt de waarde van betaald advies kunnen ontdekken.

Beslissing. Betaald advies bovenop de gratis route:

  • Opiniegevers kunnen een Adviesniveau hebben (1–4 + Expert), met prijzen per beoordeling als beheerinstelling (niveauPrijzen, start €0,20/0,40/0,60/0,80/1,00). Het niveau volgt uit de sterren-reputatie (ADR-027) via vaste drempels: elke sterrenPerNiveau (30) sterren = een niveau hoger — 30/60/90/120★; ⚠️ Expert = 150★ én handmatig/op aanvraag.
  • Vanaf niveau 1 is het tekstvak in het opiniescherm vrijgegeven én verplicht (gemodereerd zoals alle vrije tekst).
  • De vraagsteller kiest bij zijn vraag per niveau een aantal beoordelaars (niveaueisen, som ≤ 10 ⚠️); totaalkosten = som van (aantal × niveauprijs), vooraf getoond en op de vraag bevroren, betaald uit het euro-saldo (ADR-029).
  • Teaser-flow: zonder account/saldo/niveau-instelling krijgt de vraag tóch minimaal minTeaserNiveauBeoordelaars (standaard 2, best effort) niveau-beoordelaars; hun tekst verschijnt geblurd in de uitslag met niveau + prijs. De vraagsteller koopt er hoogstens één (Opinie.ontgrendeld), met on-the-fly het simpelste account + iDeal. De beoordelaar wordt alleen betaald bij aankoop (niet gekocht = geen betaling). De verdeelsleutel is 50/50 (beoordelaarAandeelProcent, standaard 50).

Status. Vastgesteld (2026-07-08b; geverifieerd door Han).

Gevolgen.

AspectGevolg
VerdienmodelGoede beoordelaars verdienen echt geld; het platform krijgt een inkomstenstroom (verdeelsleutel 50/50).
Gratis route intactSmiley-scores en de gratis (gevraagde) reactie veranderen niet; betaald advies is additief.
ConversieDe geblurde teaser toont elke vraagsteller wat er te koop is, zonder gratis weg te geven.
BeperkingVereist voldoende niveau-beoordelaars (teaser is best effort) en een uitgewerkte niveau-toekenning (via sterren — te bevestigen).
ImplementatieSnapshots (adviesniveauSnapshot, prijsSnapshot, ontgrendeld) op de Opinie; niveaueisen (map niveau→aantal) + adviesTotaalprijs op de Vraag; ontgrendel-endpoint met hoogstens één ontgrendelde teaser per vraag.

Zie Betaald advies, het scenario Betaald advies & teaser en Backend & API.


ADR-029 — Euro-saldo met iDeal (via een PSP) en eenmalige welkomstbonus — geld keert terug

Section titled “ADR-029 — Euro-saldo met iDeal (via een PSP) en eenmalige welkomstbonus — geld keert terug”

Geverifieerd door Han (2026-07-08); parameters vastgesteld in 2026-07-08b. Welkomstbonus max €5 (totaal max €10), uitbetaling via het profielscherm vanaf €10. Overrulet het “geen geld in het systeem”-besluit van 2026-07-03 (ADR-019). Zie het voorstel-document, §4–§5.

Context. Betaald advies vereist echt geld. ADR-019 sloot geld expliciet uit (complexiteit/compliance); de stakeholder-notities van 3-7-2026 draaien dat om: er komt een inleg, een bonus en een uitbetaling. De anonimiteitsbelofte moet daarbij houdbaar geherformuleerd worden.

Beslissing.

  • Naast credits krijgt een account een euro-saldo (“inleg”), op te laden via iDeal bij een payment service provider (PSP; achter een applicatie-poort, met geverifieerde en idempotent verwerkte webhooks). Het platform bewaart alleen transactiereferentie + saldo — bank-/kaartgegevens leven bij de PSP.
  • Eenmalige welkomstbonus bij accountcreatie: de eerste inleg wordt verdubbeld tot max. welkomstbonusMaxEuro (standaard €5) bonus, zodat het totaal max €10 is (€5 gebruiker + €5 I See You).
  • Credits en euro’s strikt gescheiden: geen omwisselkoers.
  • De Uitbetaling keert terug: aan te vragen via het profielscherm zodra het saldo uitbetaalDrempelEuro (standaard €10) is; de frequentie is niet vastgelegd. Sterren hebben geen directe geldwaarde — ze ontsluiten het adviesniveau, dat de uitbetaling per beoordeling bepaalt. Bij uitbetalen gelden KYC-verplichtingen (bankrekening).
  • Anonimiteit geherformuleerd: anoniem richting andere gebruikers; betaalgegevens bij de payment provider; platform bewaart het minimaal noodzakelijke.

Status. Vastgesteld (2026-07-08b; geverifieerd door Han; overrulet ADR-019 deels).

Gevolgen.

AspectGevolg
Lage drempeliDeal is de NL-standaard; de welkomstbonus verlaagt de instap.
ComplianceBetalingen/KYC via de PSP beperken de eigen compliance-last; toch keert een deel van de financiële administratie terug die ADR-019 juist vermeed — bewuste stakeholder-keuze.
AnonimiteitHoudbaar geherformuleerd (richting andere gebruikers); dataminimalisatie: alleen referentie + saldo in het platform.
Zuivere economieDe strikte scheiding houdt de gratis credits-cyclus onaangetast.
BeperkingDe PSP-keuze is inmiddels ingevuld — Mollie (ADR-035); KYC + DAC7 bij uitbetaling blijven open (latere story). De vastgestelde parameters zijn in de code doorgevoerd (2026-07-08); de inleg-/teaser-flow gaat van gesimuleerde iDeal naar echte Mollie-betalingen.

Zie Betaald advies, Beveiliging & privacy en Tech stack.


ADR-030 — Onboarding met eerst-2-beoordelen-poort (met vangrail) en default terug naar 10

Section titled “ADR-030 — Onboarding met eerst-2-beoordelen-poort (met vangrail) en default terug naar 10”

Geverifieerd door Han (2026-07-08) en gebouwd. Zie het voorstel-document, §6–§7. De onboarding-poort (met cold-start-vangrail), de welkomsttekst en de tips-vensters zitten in de code (2026-07-08; welkomsttekst en tips als beheerbare Keuzelijst-content).

Context. Nieuwe gebruikers begrijpen de wederkerigheid (beoordelen ↔ beoordeeld worden) niet vanzelf. De welkomsttekst van blad 2 (notitie 3-7-2026) legt die expliciet vast: eerst zelf 2 vragen beantwoorden vóór je eigen vraag wordt voorgelegd aan 10 mensen — precies genoeg credits voor de inzage. Een vergelijkbare poort is in 2026-06-30b juist afgeschaft wegens een cold-start-deadlock; de default aantalOpiniegevers stond bovendien sinds 3-7-2026 op de test-waarde 1, terwijl de welkomsttekst met 10 rekent.

Beslissing.

  • Onboarding: welkomsttekst (beheerbare content) bij het eerste bezoek; de eerste vraag van een nieuwe gebruiker gaat pas de verdeling in nadat hij onboardingAantalBeoordelingen (standaard 2) vragen van anderen beantwoordde (2 × 5 = 10 credits = de inzage-prijs bij 10).
  • Cold-start-vangrail (interpretatie, expliciet ter verificatie): is er geen beoordeelbaar werk beschikbaar, dan vervalt de poort tijdelijk en gaat de vraag direct de pool in — zo blijft de bedoeling overeind zonder de deadlock die de eerdere poort de kop kostte.
  • Tips-vensters: klikken op de tekst van een deelvraag in het opiniescherm opent een venster met tips/uitleg (beheerbare content per deelvraag en taal).
  • Default aantalOpiniegevers terug van 1 naar 10 (draai van het default-deel van ADR-026); de tarieven verdienen-5/inzage-10 blijven; alles blijft instelbaar.

Status. Voorgesteld (2026-07-05, ter verificatie). Nuanceert ADR-017 (credits als enige poort) en draait het default-deel van ADR-026 terug.

Gevolgen.

AspectGevolg
WederkerigheidNieuwe gebruikers dragen aantoonbaar bij vóór ze consumeren, en verdienen precies genoeg voor hun eigen uitslag.
Geen deadlockDe vangrail voorkomt het kip-en-ei-probleem van de eerder afgeschafte poort.
KwaliteitTips-vensters helpen (nieuwe) beoordelaars goed en eerlijk te scoren.
BeperkingDe poort geldt alleen voor de eerste vraag; de vangrail moet betrouwbaar “geen werk beschikbaar” detecteren.
ImplementatieInstellingen onboardingAantalBeoordelingen en default aantalOpiniegevers = 10; beheerbare welkomst-/tipscontent.

Zie Onboarding, Opinie geven en Systeembeheer.


ADR-031 — In-app auth met ondertekende bearer-tokens (Authorization-header i.p.v. cookies)

Section titled “ADR-031 — In-app auth met ondertekende bearer-tokens (Authorization-header i.p.v. cookies)”

Context. ADR-007 en Beveiliging & privacy beschrijven de doelarchitectuur voor authenticatie: ASP.NET Core Identity voor beheerders, custom anonieme auth voor gebruikers, gedragen door een single-device cookie-sessie (HttpOnly/Secure/SameSite=Strict) en autorisatiebeleid IsBeheerder. Bij het daadwerkelijk bouwen bleek de cookievorm onwerkbaar voor de gekozen deploytopologie: de twee portalen (Static Web Apps op *.azurestaticapps.net) draaien op een andere site dan de API (App Service op *.azurewebsites.net). Een SameSite=Strict/Lax-cookie wordt cross-site niet meegestuurd, en SameSite=None zou de CSRF-bescherming die de cookie-flags juist moesten bieden weer weghalen. Tegelijk was in de eerste implementatiestap de autorisatie feitelijk afwezig: de beheer-endpoints waren onbeschermd en de gebruikers-endpoints vertrouwden een als tekst meegegeven actor — de review van 2026-07-08 toonde dat credits toekennen, de gebruikerslijst (met e-mail) opvragen en systeeminstellingen wijzigen zonder enige authenticatie mogelijk waren.

Beslissing. De autorisatie uit ADR-007/ADR-013 wordt afgedwongen via HMAC-ondertekende bearer-tokens in de Authorization-header in plaats van cookies:

  • Beheerder-token. POST /api/beheer/login verifieert de admin-credentials uit het Systeem-aggregate (constant-time) en geeft een ondertekend admin-token terug. Alle wijzigende/gevoelige /api/beheer/*-endpoints zitten achter een admin-guard (endpoint-filter) die dit token vereist → anders 401. Uitzonderingen die publiek blijven: de login zelf en de read-only GET /api/beheer/instellingen (het gebruikersportaal heeft de niet-gevoelige parameters/prijzen nodig).
  • Actor-token. Login en het starten van een anonieme sessie geven een actor-token terug dat de actor-referentie (gebruikersnaam of anon:<guid>) draagt. Alle gebruikers-endpoints (profiel, saldo, mijn vragen, vraag stellen/ontgrendelen/verwijderen/melden, keuzeset/kiezen/opinie, waardering, betalingen) zitten achter een actor-guard en leiden de actor server-side uit het token af in plaats van een meegegeven actor-parameter te vertrouwen → geen impersonatie/datalek meer. De admin- en actor-tokens hebben een aparte type-tag en zijn niet uitwisselbaar.
  • Tokens zijn HMAC-SHA256-ondertekend met een servergeheim (Auth:Secret, in productie verplicht te zetten), bevatten een type-tag (admin vs. actor, niet uitwisselbaar) en een vervaltijd; validatie is constant-time. Ze zijn stateless (geen serveropslag).

Status. Aanvaard (2026-07-08) — implementatiekeuze die de cookievorm uit ADR-007/ADR-011 vervangt voor de cross-origin-topologie. ASP.NET Core Identity blijft het bredere doelbeeld voor het cookiegebaseerde deel; de wachtwoord-hashing is per 2026-07-11q naar Argon2id overgestapt (zie de gevolgentabel), los van de vraag óf/wanneer het cookie-/Identity-deel nog volgt.

Gevolgen.

AspectGevolg
BeveiligingDe gedemonstreerde exploits (creditfraude, PII-dump via de gebruikerslijst, config-tampering, willekeurig verwijderen) zijn dicht: beheer-endpoints geven zonder geldig admin-token 401.
Cross-originEen bearer-header werkt wél tussen de SWA-portalen en de App-Service-API; geen SameSite-beperking.
BeperkingEen bearer-token in localStorage is leesbaar vanuit JavaScript (anders dan een HttpOnly-cookie) — de krappe CSP blijft daarom belangrijk als XSS-mitigatie. Token-revocatie is (nog) tijdgebonden (vervaltijd), niet server-side ingetrokken.
Afwijking van doelCookies (ADR-007/011) + volledige ASP.NET Core Identity blijven het doelbeeld voor het beheerder-auth-model; deze ADR legt de pragmatische, cross-origin-bestendige tussenstap vast zodat docs en code in lijn zijn. De wachtwoord-hashing zelf is inmiddels wél op het doelniveau (Argon2id, per 2026-07-11q).
ConfigAuth:Secret moet in productie gezet worden; lokaal is er een dev-default.

Zie Beveiliging & privacy en Backend & API.


ADR-032 — Observability: Serilog-gestructureerde logging + Application Insights

Section titled “ADR-032 — Observability: Serilog-gestructureerde logging + Application Insights”

Context. De API logde tot nu toe met losse Console.Error.WriteLine-regels: ongesorteerd, zonder request-context en nergens doorzoekbaar bewaard. Voor een pilot met één tester volstond dat; voor publiek gebruik (zie de routekaart, fase 1) is opschalen zonder metrics, tracing en alerting blind varen. De fail-open/fail-graceful patronen van de externe afhankelijkheden (OpenAI-moderatie, vision-AI, ACS-e-mail) maken het extra belangrijk dat stille terugvallen zichtbaar zijn: een vision-route die wekenlang ongemerkt terugvalt op het deterministische vangnet (zoals bij punt 11 van Han gebeurde) hoort in een dashboard op te vallen.

Beslissing.

  • Serilog vervangt de console-writes: één gestructureerde logging-pijplijn (ILogger<T> overal, ook in de Infrastructure-diensten), met request-logging via UseSerilogRequestLogging. Lokaal leesbare console-output; in productie compacte JSON.
  • Application Insights als APM: AddApplicationInsightsTelemetry verzamelt requests, dependencies en exceptions; de Serilog-events gaan als traces mee via de Application Insights-sink. De koppeling loopt uitsluitend via de standaard app-setting APPLICATIONINSIGHTS_CONNECTION_STRINGzonder die setting draait de app ongewijzigd en logt hij alleen naar de console (fail-graceful, zoals de andere integraties).
  • Basis-alerting: één metric-alert op het aantal mislukte requests, gekoppeld aan een actiongroep met e-mailnotificatie. Uitbreiding (beschikbaarheidstests, dashboards) volgt op basis van echt verkeer.

Status. Aanvaard (2026-07-11) — implementatiekeuze, geen modelwijziging.

Gevolgen.

AspectGevolg
InzichtRequests, fouten, afhankelijkheids-latency (OpenAI, ACS, Postgres) en de vision/vangnet-terugvallen zijn doorzoekbaar in Application Insights.
Fail-graceful zichtbaarDe “stille” terugvallen loggen nu gestructureerd (met properties i.p.v. tekstprefixen als [ai-persona]), dus er is op te alerten en te dashboarden.
KostenApplication Insights rekent per GB ingest; bij pilot-volumes verwaarloosbaar. De sampling-standaarden van de SDK blijven aan.
Cloud-agnostischSerilog is neutraal; alleen de sink/APM-koppeling is Azure-specifiek en zit achter één omgevingsvariabele — een andere APM is een sink-swap, geen herontwerp (in lijn met ADR-015).
PrivacyLogs bevatten actor-referenties en technische metadata, nooit opname-inhoud, wachtwoorden of e-mailadressen; de retentie van de APM-werkruimte is eindig (90 dagen standaard).

Zie Deployment.


ADR-033 — CI/CD op een self-hosted runner

Section titled “ADR-033 — CI/CD op een self-hosted runner”

Context. De GitHub-hosted runners van deze organisatie/repo bleken niet beschikbaar (elke CI-run op een GitHub-hosted runner faalde binnen enkele seconden — uitgeputte of uitgeschakelde runner-minuten). Daarnaast liep de deploy naar Azure (API, portalen, documentatiesite) tot nu toe als handmatige stap (az-commando’s, gedocumenteerd in de pak-issue-skill) los van de CI-pijplijn: geverifieerde code kon zo ongemerkt achterblijven bij wat er live staat.

Beslissing.

  • CI/CD draait voortaan op een self-hosted Windows-runner, geregistreerd op deze repo en fysiek draaiend op een ontwikkelaarslaptop (actions/runner, gestart via run.cmd, gemonitord/herstart via de deploy-skill).
  • Dezelfde pijplijn (.github/workflows/ci.yml) krijgt naast de bestaande verificatiejobs (docs/app/backend) drie deploy-jobs: deploy-docs, deploy-app (beide portalen), deploy-api. Ze draaien alleen bij een push naar main (nooit bij een pull request) en alleen voor de onderdelen waarvan bestanden wijzigden (bepaald via git diff tussen de vorige en de nieuwe commit).
  • De deploy-jobs gebruiken de al ingelogde az-CLI-sessie op de runner om credentials live op te vragen (az acr credential show, az staticwebapp secrets list) — er staan dus geen Azure-secrets in GitHub. De API-image wordt getagd op de commit-SHA (sha-<kort-sha>), los van de handmatige vN-reeks uit de pak-issue-skill (beide tag-schema’s leven naast elkaar in dezelfde ACR-repository zonder conflict).
  • De API-deploy sluit af met een gezondheidscontrole (pollen van /api/beheer/instellingen tot 200) vóórdat de job slaagt.
  • De handmatige az-deploy-stappen in de pak-issue-skill blijven bestaan als fallback/noodpad, niet als het normale pad.

Status. Aanvaard (2026-07-11) — implementatiekeuze, geen modelwijziging.

Gevolgen.

AspectGevolg
BetrouwbaarheidVerificatie en deploy staan niet meer los van elkaar: wat groen is op main, staat (voor de gewijzigde onderdelen) ook automatisch live.
Beschikbaarheid van de pijplijnDe pijplijn is afhankelijk van één laptop (geen hoge beschikbaarheid); zonder draaiende runner blijft een push wachten tot iemand hem weer start.
SecretsGeen Azure-credentials in GitHub-secrets — kleiner lek-oppervlak, maar wel een afhankelijkheid van de interactieve az login-sessie op die specifieke laptop.
Cloud-agnostischOnveranderd: de deploy-stappen zijn nog steeds standaard dotnet/az/SWA-CLI-commando’s; alleen de uitvoerder (self-hosted i.p.v. GitHub-hosted) verandert.
Handmatig padDe pak-issue-skill-stappen blijven werken als expliciete fallback (bijv. als de runner-laptop niet beschikbaar is).

Zie Deployment.


ADR-034 — Visuele moderatie van opnames vóór distributie (fail-open)

Section titled “ADR-034 — Visuele moderatie van opnames vóór distributie (fail-open)”

Context. De moderatie dekte tot nu toe twee vormen: reactief melden van een ongepaste vraag (drempel → automatische verwijdering) en proactieve tekstmoderatie van alle vrije tekst (ADR-020). De opname zelf — juist de kern van het product — ging echter ongescreend naar de opiniegevers: een ongepaste foto of video werd pas na meldingen van de opiniegevers verwijderd, die er dan al aan waren blootgesteld. Voor publiek gebruik (routekaart fase 0, top-5 nr. 1) is dat onhoudbaar: de eerste verdedigingslinie hoort vóór distributie te liggen, ook ter bescherming van de opiniegevers zelf.

Beslissing.

  • Elke opname wordt bij het stellen van de vraag, vóór opname in de pool gescreend via een nieuwe applicatie-poort IVisueleModeratieDienst (ControleerAsync(mediaType, mediaSleutel) → ModeratieUitslag). Een foto gaat direct als beeld-input naar de OpenAI Moderation API (omni-moderation-latest, dezelfde gratis API en sleutel als de tekstmoderatie); uit een video haalt FFmpeg ~4 frames (hergebruik van de bestaande frame-extractie van de vision-AI, ADR-024).
  • Afgekeurd → geweigerd + direct wissen. De vraag wordt geweigerd met een nette melding (zelfde patroon als afgekeurde tekst) en de zojuist geüploade opname wordt direct uit de mediastore verwijderd — er blijft niets achter en er vindt geen distributie plaats.
  • Fail-open, consistent met de tekstmoderatie: zonder API-sleutel, bij een niet-ondersteund beeldformaat of bij een API-/netwerkfout wordt de opname toegestaan — de app blijft werken en de terugval is zichtbaar in de logs (ADR-032). Het reactieve melden blijft als tweede linie bestaan.
  • Beheerinstelling visueleModeratieActief (bool, standaard aan) op de Systeeminstellingen zet de check aan/uit.

Status. Aanvaard (2026-07-11g).

Gevolgen.

AspectGevolg
VeiligheidOngepaste opnames bereiken de opiniegevers niet meer; melden wordt tweede linie i.p.v. enige linie.
PrivacyDe beelden gaan uitsluitend naar de al gebruikte OpenAI-API (zelfde verwerker als de vision-AI); er wordt niets extra bewaard — bij afkeuring wordt de opname juist direct gewist.
Kosten & latentieDe Moderation API is gratis; het stellen van een vraag krijgt één extra API-call (foto) of een FFmpeg-run + call (video) — orde één seconde.
BeperkingFail-open betekent: bij storing of ontbrekende sleutel géén visuele screening (bewuste keuze — beschikbaarheid boven strengheid in de pilotfase); een fail-closed-stand kan later een instelling worden.
TestbaarheidDe poort is in integratietests vervangbaar (stub), zodat de weiger-flow (400 + gewiste opname) zonder echte API getest wordt.

Zie Moderatie.


ADR-035 — Mollie als PSP-implementatie; betaalpoort async + status-opvraag

Section titled “ADR-035 — Mollie als PSP-implementatie; betaalpoort async + status-opvraag”

Concretiseert de PSP-keuze uit ADR-029 (routekaart fase 1, lanceerblokker). De inleg- en teaser-ontgrendel-flow gaat van de gesimuleerde (dummy) iDeal naar een echte Mollie-betaling. De uitbetaling aan beoordelaars (KYC/DAC7) blijft bewust buiten scope van deze stap.

Context. ADR-029 besloot euro-saldo via iDeal bij een PSP, achter een applicatie-poort, maar liet de PSP-keuze open en werd gebouwd met een dummy die redirect + webhook nabootst. Voor lancering (geen echt geld = geen echt verdienmodel) is een echte PSP nodig. iDeal is de Nederlandse consumentenstandaard; de markt is NL-eerst en EUR; de bedragen zijn klein (€0,20–€1,00 per beoordeling) en het opladen is prepaid. Mollie is daarvoor de beste keuze: iDeal-thuisspel, laagste frictie/kosten, geen lock-in, en de bestaande poort is al op de PSP-vorm (redirect + idempotente webhook) gebouwd.

Beslissing.

  • Mollie wordt de PSP-implementatie achter IBetaalPoort. De dummy (DummyBetaalDienst) blijft bestaan als terugval voor lokaal/CI (zonder Mollie:ApiKey wordt de dummy geregistreerd, mét sleutel de Mollie-adapter).
  • De poort wordt uitgebreid omdat een echte PSP anders werkt dan de dummy: Mollie roept de webhook server-to-server aan met alléén een payment-id (geen status in de body). IBetaalPoort krijgt daarom een status-opvraag en Start wordt async (de Mollie-call is netwerk): StartAsync(...) → BetaalStart(PspReferentie, RedirectUrl) en IsGeslaagdAsync(pspReferentie) → bool? (null = nog niet definitief).
  • De status wordt altijd bij Mollie opgehaald (GET /v2/payments/{id}); de webhook-body wordt nooit vertrouwd. Een nieuwe publieke endpoint POST /api/betalingen/psp-webhook leest het id, vraagt de status op en roept de bestaande, idempotente BetaalService.WebhookAsync(referentie, geslaagd) aan. De bestaande body-based /api/betalingen/webhook blijft voor de dummy.
  • Secrets: Mollie:ApiKey en de publieke API-basis-URL (App:ApiBaseUrl, voor de webhookUrl) via Azure-config/Key Vault — zelfde patroon als OpenAI__ApiKey. Het platform bewaart nog steeds alleen transactiereferentie + saldo.
  • Buiten scope (latere story): geautomatiseerde uitbetaling aan beoordelaars (een IUitbetaalPoort met Mollie Connect of een heroverweging van Stripe Connect/Adyen), inclusief KYC en de DAC7-rapportageplicht. De poort houdt die keuze open.

Status. Aanvaard (2026-07-11h). Implementatiekeuze die de open PSP-keuze uit ADR-029 invult; raakt het domeinmodel niet (poort-/adapterlaag).

Gevolgen.

AspectGevolg
Lage frictieiDeal via Mollie is de NL-standaard: beste conversie, laagste kosten voor micro-bedragen, geen maandvast/lock-in.
BeveiligingDe uitkomst komt van een server-to-server statuscheck bij Mollie, niet van de (spoofbare) webhook-body; verwerking blijft idempotent op de referentie.
TerugvalZonder Mollie:ApiKey blijft de dummy actief, dus lokaal/CI draaien zonder PSP-account; de flow (redirect → webhook) is identiek.
InwisselbaarBlijft achter de poort; een latere overstap naar Stripe/Adyen voor de marktplaats-/uitbetaalkant is een adapter, geen herontwerp.
BeperkingUitbetaling/KYC/DAC7 zijn nog niet geautomatiseerd; de definitieve PSP-keuze en het DAC7-traject zijn óók een commerciële/juridische beslissing (met de opdrachtgever te bevestigen vóór de live-sleutel).

Zie Betaald advies, Tech stack en de Routekaart (fase 1).


ADR-036 — First-party product-analytics (cookieloos, zonder PII, geen externe dienst)

Section titled “ADR-036 — First-party product-analytics (cookieloos, zonder PII, geen externe dienst)”

Context. De routekaart (fase 0, top-5 nr. 5) constateert dat er geen enkel funnel-inzicht is: hoeveel bezoekers openen het portaal, hoeveel halen de onboarding-poort, hoeveel stellen een vraag? Zonder meten is groei-sturing gokken. Externe analytics-diensten (PostHog EU, Plausible) zijn volwassen, maar brengen een extern account, kosten, een verwerkersovereenkomst en (bij de meeste) een cookie-/consent-vraag mee — wringend met de kernbelofte anonimiteit van dit product.

Beslissing. First-party, minimale event-registratie in de eigen stack:

  • Eén publiek ingest-endpoint POST /api/events met een strikte allowlist van gebeurtenistypen (de funnel-stappen: portaal_geopendwelkom_gezienpoort_gestartbeoordeling_gegevenvraag_gesteldresultaat_ontgrendeld); onbekende typen worden geweigerd.
  • Geen PII, geen cookies: een gebeurtenis is (type, meetId, tijdstip). De meetId is een door de client gegenereerde, willekeurige GUID per browsersessie (sessionStorage) — niet gekoppeld aan een account, sessie of IP. Uniek-tellen kan; herleiden niet.
  • Opslag in de bestaande PostgreSQL (tabel Meetgebeurtenissen), bewust in de Infrastructure-laag: dit is telemetrie over het product, geen domeinmodel — het domein blijft er vrij van.
  • Beperkte bewaartermijn: een dagelijkse opruimtaak verwijdert gebeurtenissen ouder dan 90 dagen.
  • Beheer-inzicht: GET /api/beheer/statistieken (admin-token) levert aantallen + unieke meet-id’s per dag per type; het beheerportaal toont dit in de nieuwe tab Statistieken.

Status. Aanvaard (2026-07-11) — implementatiekeuze; geen modelwijziging (telemetrie, geen domein).

Gevolgen.

AspectGevolg
PrivacyGeen cookies, geen PII, geen data naar derden — geen consent-banner nodig en volledig in lijn met de anonimiteitsbelofte.
Kosten & beheerGeen extern account of abonnement; de data staat in de bestaande database en valt onder de bestaande back-ups.
BeperkingGeen kant-en-klare dashboards/cohort-analyses zoals PostHog; bewust minimaal — als de behoefte groeit is een export of een latere overstap mogelijk (de events zijn een simpele tabel).
MisbruikHet ingest-endpoint is publiek; de allowlist + het ontbreken van vrije velden beperken de schade van spam tot ruis in de tellingen. Rate limiting kan later meeliften op de API-brede rate limiting (routekaart fase 1).
FunnelDe onboarding-funnel-story instrumenteert het gebruikersportaal met deze events en bouwt de funnel-weergave in beheer.

Zie Systeembeheer — Statistieken.


ADR-037 — Land vervalt (single-tenant per site); Regio wordt een vlakke keuzelijst

Section titled “ADR-037 — Land vervalt (single-tenant per site); Regio wordt een vlakke keuzelijst”

Context. De koers is gewijzigd (routekaart, positionering): I See You wordt per land als aparte site gehost, elk op een eigen domein — de eerste (en vooralsnog enige) focus is Nederland. Daarmee is een door de gebruiker gekozen Land-veld (zie ADR-016) overbodig: welk land het betreft volgt uit welke site de gebruiker bezoekt, niet uit een profielkeuze. De UN M49-verbredingslogica die ADR-016 beschreef (land → subregio → werelddeel) was bovendien nooit geïmplementeerd in de matching — alleen de exacte landmatch was gebouwd. Regio (per land beheerbare stamdata, sleutel "<landcode>.<regiosleutel>", sinds 2026-07-10d) had Land dus nog wel nodig als naamgevingsprefix, zonder dat er nu nog een reden is om meerdere landen tegelijk te ondersteunen.

Beslissing. Land vervalt volledig als profielveld en doelgroepcriterium:

  • Profiel/ProfielSnapshot, AnoniemeGebruiker, SelectieCriteria en PersonaProfiel verliezen het veld Land. Er is geen landkeuze meer bij registratie, profiel, doelgroepcriteria of AI-persona’s.
  • Regio wordt een vlakke, site-brede keuzelijst (keuzelijst-type regio) zonder landprefix — de sleutel is voortaan gewoon de regionaam (bijv. gelderland), niet meer "nl.gelderland". De matching op regio (binnen de site, dus binnen NL) is ongewijzigd: een regio-criterium beperkt tot dezelfde regio, verbreedt niet.
  • Het keuzelijst-type land vervalt (geen landenlijst meer te beheren).
  • RegioM49 (afgeleide UN M49-regio, ADR-016) vervalt — die was toch al niet geïmplementeerd in de matching.
  • Elke toekomstige uitbreiding naar een ander land krijgt een eigen site/deployment (eigen domein, eigen database), niet een Land-veld binnen dezelfde applicatie — zie de routekaart (fase 3, internationalisatie).

Status. Aanvaard (2026-07-11m) — modelwijziging.

Gevolgen.

AspectGevolg
EenvoudEén minder verplicht profielveld bij registratie; regiosVanLand-achtige land→regio-filtering in de UI vervalt.
AnonimiteitNog minder persoonsgegevens nodig — Land was al geen locatie-tracking, maar is nu ook geen los datapunt meer.
DataBestaande Regio-waarden met een landprefix (bijv. "nl.gelderland") verliezen hun match met de herseeded vlakke keuzelijst; bij het volgende profiel-/vraagbewerking kiest de gebruiker opnieuw. Acceptabel gezien de beperkte pilotomvang.
InternationalisatieUitbreiding naar een ander land wordt een nieuwe site/deployment (apart domein + database) in plaats van een Land-keuze binnen dezelfde app — sluit aan bij de “per land een site”-koers.

Zie Domeinmodel — Gebruiker & Toegang en de routekaart.


ADR-038 — Audittrail voor beheer-support (mét actor-identiteit, admin-only)

Section titled “ADR-038 — Audittrail voor beheer-support (mét actor-identiteit, admin-only)”

Context. Issue #22: de beheerder heeft geen manier om achteraf te herleiden wie welke vraag stelde, wie wat beoordeelde, en wie wanneer zijn euro-saldo ophoogde — nodig om als beheerder goed support te kunnen verlenen (bv. bij een klacht, een betwiste transactie, of misbruik-onderzoek). De bestaande first-party product-analytics (ADR-036) is bewust anoniem (geen actor-identiteit) — geschikt voor productmetrics, niet voor support. Dit is dus een bewust ander soort log, met een eigen afweging.

Beslissing. Een nieuwe, admin-only Auditgebeurtenis-log, uitsluitend metadata (nooit media/opnames — die blijven onderworpen aan het bestaande retentiebeleid):

  • Wél actor-identiteit (gebruikersnaam of anon:<sessie-id>), in tegenstelling tot ADR-036 — dit is een bewuste, beperkte uitzondering op “geen persoonsgegevens zonder noodzaak”, gerechtvaardigd door het support-doel en afgeschermd doordat alleen de beheerder de audittrail kan inzien (GET /api/beheer/audittrail, achter het bestaande admin-token, ADR-031).
  • Wat wordt gelogd (elk record: type, actor, optioneel target/details, tijdstip): registratie, vraag gesteld, opinie gegeven, waardering gegeven, euro-saldo opgehoogd (bevestigde iDeal-inleg), uitbetaling aangevraagd, account-upgrade (anoniem → geregistreerd), melding ingediend, vraag vervallen door meldingen, en (per 2026-07-13, ADR-040) mislukte login (MislukteLogin, met de reden in details). Elke schrijfactie gaat mee in dezelfde databasetransactie als de business-mutatie die ze beschrijft (via de bestaande IUnitOfWork, net als andere repository-writes) — geen fire-and-forget, dus nooit een audit-regel zonder de bijbehorende mutatie of andersom.
  • Eigen, langere retentie: een nieuwe beheerinstelling auditRetentieDagen (standaard 365) — bewust ruimer dan de 90 dagen van de anonieme Meetgebeurtenissen (ADR-036), omdat support vaak met vertraging wordt gevraagd, maar nog altijd begrensd (privacy-by-design, geen eeuwige bewaring). Een AuditOpruimHostedService (zelfde patroon als MeetOpruimHostedService) ruimt periodiek op.
  • Nooit de verwijderde opname/media zelf — alleen id’s/aantallen/bedragen. De audittrail overleeft dus bewust de retentie van de onderliggende Vraag, maar bevat zelf nooit de inhoud die geretentioneerd wordt.
  • Beheer-UI: nieuwe tab Audittrail (filters op actor, type en periode) — zie Systeembeheer.

Status. Aanvaard (2026-07-11n) — modelwijziging.

Gevolgen.

AspectGevolg
SupportDe beheerder kan achteraf herleiden wie wat deed — essentieel bij klachten, betwiste transacties en misbruik-onderzoek.
PrivacyBewuste, beperkte uitzondering op de anonimiteitsbelofte: identiteit is zichtbaar, maar uitsluitend voor de beheerder, nooit media, en met een eigen, begrensde retentie (niet oneindig).
ConsistentieAudit-writes gaan mee in dezelfde transactie als de mutatie — geen risico op een audittrail die uit de pas loopt met de werkelijke data.
BeheerlastEén nieuwe instelling (auditRetentieDagen) en één nieuwe achtergrondtaak, naar het bestaande MeetOpruimHostedService-patroon.

Zie Domeinmodel — Beheer en Systeembeheer — Audittrail.


ADR-039 — Infrastructure as Code met Pulumi; meerdere omgevingen (voorstel)

Section titled “ADR-039 — Infrastructure as Code met Pulumi; meerdere omgevingen (voorstel)”

Context. Issue #24: er is nu precies één omgeving — resourcegroep Clermond (gedeeld met andere projecten van de opdrachtgever, zoals padel-firmware en de zelf-hostende CI-runner-VM) — en geen infrastructure-as-code: alle Azure-resources zijn met losse az-CLI-commando’s aangemaakt (zie de handmatige stappen in de pak-issue- en deploy-skills). De opdrachtgever wil op termijn minimaal een test-, preproductie- en productieomgeving, met een pijplijn die pas naar preproductie promoveert na een volledige end-to-end-testrun. Dit sluit aan bij Fase 1 van de routekaart (“CD-pipeline + IaC… een aparte staging-omgeving”).

Beslissing — twee delen, met een bewust verschillende status:

  1. IaC-tool: Pulumi, TypeScript (Aanvaard, nu gebouwd). De opdrachtgever stelde zelf self-hosted Pulumi voor (state in eigen beheer, geen Pulumi Cloud-abonnement). Taalkeuze TypeScript: dat is al de dominante taal in deze repo (beide frontends, de docs-site, de e2e-/live-verify-scripts) — lagere drempel dan een aparte Bicep-/Terraform-DSL of C# (dat de deploy-tooling zelf niet gebruikt). De bestaande productie-resources voor I See You zijn gecodificeerd in infra/ (zie hieronder) — puur beschrijvend (geen pulumi up/import tegen de echte Azure-omgeving uitgevoerd; dat is een aparte, bewuste vervolgstap zodra de state-backend gekozen is).
  2. Test-/preproductie-omgevingen (apart van productie) (Voorgesteld, ter beoordeling). Nieuwe omgevingen betekenen nieuwe, doorlopend betaalde Azure-resources (minstens een extra App Service-plan + PostgreSQL-server per omgeving) in een gedeelde resourcegroep van de opdrachtgever — een kostenbeslissing die niet zonder expliciete instemming wordt genomen. Voorstel: twee extra resourcegroepen (Clermond-Test, Clermond-Preprod) met dezelfde Pulumi-stack, geparametriseerd per omgeving (SKU’s een tandje lager dan productie waar dat kan). De pijplijn (.github/workflows/ci.yml) promoveert automatisch naar test bij elke push naar main (zoals nu naar productie), draait daar de volledige Playwright-e2e-suite (nu alleen lokaal/handmatig via de pak-issue-skill), en promoveert pas na een groene e2e-run — handmatig goedgekeurd — door naar preproductie; productie blijft een expliciete, handmatige promotie.

Status. Deel 1 Aanvaard (2026-07-11o) — implementatiekeuze, geen modelwijziging. Deel 2 was Voorgesteld en is per 2026-07-14b Vervangen door ADR-044: geen aparte resourcegroep per omgeving, maar meerdere omgevingen — met een omgevingslabel in elke resourcenaam — in één resourcegroep (Clermond-2).

Gevolgen.

AspectGevolg
HerhaalbaarheidDe productie-infrastructuur staat nu in code (infra/) — reproduceerbaar, doorzoekbaar, en het uitgangspunt voor nieuwe omgevingen zodra die worden goedgekeurd.
KostenDeel 1 kost niets extra (beschrijft alleen bestaande resources). Deel 2 verhoogt de maandelijkse Azure-kosten met (ruwweg) een extra App Service-plan + PostgreSQL-server per omgeving — vandaar de aparte, voorgestelde status.
Kwaliteit vóór preprodEen verplichte, groene e2e-run vóór promotie naar preproductie vangt regressies vroeger op dan de huidige “handmatig lokaal draaien”-aanpak.
Gedeelde resourcegroepNieuwe omgevingen komen naast bestaande, niet-I See You-resources (padel-firmware, de CI-runner-VM) in dezelfde subscription — een aparte resourcegroep per omgeving houdt dat overzichtelijk en voorkomt dat opruimen van de ene omgeving de andere raakt.

Zie Deployment — Meerdere omgevingen (voorstel) en de routekaart.


ADR-040 — Login-normalisatie (getrimd + hoofdletter-ongevoelig) en uniek e-mailadres

Section titled “ADR-040 — Login-normalisatie (getrimd + hoofdletter-ongevoelig) en uniek e-mailadres”

Context. Testfeedback (Han, 13-7-2026): meerdere zelf aangemaakte accounts gaven bij inloggen “onbekende gebruikersnaam of onjuist wachtwoord”, terwijl dezelfde accounts vanaf een ander apparaat wél werkten. Oorzaak: de gebruikersnaam-opzoeking was exact (hoofdletter- én spatiegevoelig, g.Id == naam) en er was geen normalisatie bij registreren of inloggen. Op een mobiel toetsenbord maakt autocorrectie/auto-hoofdletter de eerste letter een hoofdletter of voegt een spatie toe, zodat de bij registratie opgeslagen naam stilletjes afweek van wat bij het inloggen werd getikt. Daarnaast bleek één e-mailadres aan willekeurig veel accounts te kunnen hangen, wat wachtwoordherstel per e-mail dubbelzinnig maakt. De generieke foutmelding maakte het bovendien onmogelijk om support te geven: onbekende naam en onjuist wachtwoord zijn niet te onderscheiden.

Beslissing.

  • Gebruikersnaam wordt genormaliseerd: bij registreren, account-upgrade én inloggen wordt de naam getrimd en hoofdletter-ongevoelig vergeleken. De uniciteitscheck en de login-opzoeking gebruiken die genormaliseerde vergelijking; de sessie/het actor-token gebruikt daarna de canonieke, opgeslagen schrijfwijze zodat alle downstream-opzoekingen (actor-token → Gebruiker) exact blijven kloppen. De door de gebruiker gekozen schrijfwijze blijft de weergavenaam (Id).
  • E-mailadres is optioneel maar uniek: een opgegeven adres wordt getrimd en genormaliseerd (lowercase) opgeslagen en mag aan hoogstens één account hangen. Afgedwongen op twee niveaus: een applicatiecheck (nette foutmelding bij registreren, profiel-wijzigen en account-upgrade) én een unieke database-index op het niet-lege, genormaliseerde adres (integriteit, ook bij races). Accounts zonder e-mailadres blijven onbeperkt mogelijk (NULL is niet uniek).
  • Mislukte login wordt geaudit, niet gelekt: de gebruiker krijgt onverkort één neutrale melding (geen gebruikersnaam-enumeratie), maar het systeem schrijft een Auditgebeurtenis MislukteLogin met de reden (OnbekendeGebruiker / OnjuistWachtwoord) — admin-only zichtbaar in de Audittrail (ADR-038).

Status. Aanvaard (2026-07-13) — modelwijziging (identiteitssemantiek + audittype).

Gevolgen.

AspectGevolg
InlogbaarheidVerschil in hoofdletters of omringende spaties tussen registratie en login leidt niet meer tot een mislukte login — het meest gemelde testprobleem.
MigratieDe UniekEmail-migratie is niet-destructief zelfhelend: bestaande adressen worden genormaliseerd en gedeelde adressen krijgen een +N-alias (Gmail-stijl; het oudste account houdt het kale adres) vóórdat de unieke index wordt aangemaakt. Zo verdwijnt geen account en loopt de app bij het opstarten niet vast op opzettelijk gedeelde test-accounts.
SupportDe beheerder ziet in de audittrail wélke van de twee redenen een login deed mislukken, zonder dat de gebruiker die informatie krijgt.
WachtwoordbeleidBewust buiten scope: er worden (nog) geen sterkte-eisen aan het wachtwoord gesteld — dat is een aparte story op de routekaart.

Zie Aanmelden & registratie en ADR-038.


ADR-041 — Client-side crash-rapportage (first-party, cookieloos) + auto-reload bij verouderde chunks

Section titled “ADR-041 — Client-side crash-rapportage (first-party, cookieloos) + auto-reload bij verouderde chunks”

Context. Na een deploy zag een tester een witte foutpagina (“Er ging iets mis — herlaad de pagina”) bij het registreren. Diagnose: de portalen zijn PWA’s met gehashte, lazy-geladen bundels; een reeds-open tab probeerde na de deploy een oud chunk-bestand te laden dat al vervangen was → de ErrorBoundary ving de renderfout op. Twee gaten: (1) zo’n verouderde-chunk-fout hoort geen foutpagina te geven maar simpelweg te herladen, en (2) client-side crashes waren nergens zichtbaar — de API logt naar Application Insights, maar de frontend stuurde geen telemetrie, dus bij een melding viel er niets terug te zoeken.

Beslissing.

  • Auto-reload bij verouderde chunks: beide portalen luisteren naar Vite’s vite:preloadError en doen dan één location.reload(). In combinatie met de bestaande PWA-autoUpdate verdwijnt de eenmalige crash-na-deploy.
  • First-party, cookieloze crash-rapportage: een gedeelde helper meldt onverwachte fouten (window.onerror, unhandledrejection en de ErrorBoundary) aan een nieuw publiek endpoint POST /api/client-fout, dat ze server-side via Application Insights logt (dezelfde resource als de API). Bewust géén Microsoft-browser-SDK: die zet cookies en laadt een third-party script, wat botst met het cookieloze/anonieme uitgangspunt van ADR-036. Er gaat alleen metadata mee (bron, boodschap, stacktrace, location.pathname zonder querystring, user-agent) — nooit vrije gebruikerstekst, opnames of PII — alles defensief afgekapt, met een sessielimiet tegen rapportage-lussen. Het endpoint geeft altijd 204 (rapportage mag nooit zelf falen).

Status. Aanvaard (2026-07-13) — implementatie-/observability-beslissing, geen modelwijziging.

Gevolgen.

AspectGevolg
Deploy-UXEen verouderde-chunk-fout na een deploy leidt tot een naadloze reload i.p.v. een foutpagina.
WaarneembaarheidClient-side crashes komen mét stacktrace in Application Insights — voortaan te diagnosticeren i.p.v. te gissen.
PrivacyConsistent met ADR-036: cookieloos, geen third-party SDK, alleen metadata zonder PII (pad zonder querystring).

Zie ADR-036 en Frontend.


ADR-042 — Beheerbare e-mailteksten per mailtype, met placeholder-contract

Section titled “ADR-042 — Beheerbare e-mailteksten per mailtype, met placeholder-contract”

Context. Issue #26: de tekst van het enige e-mailseintje dat de app verstuurt — het uitslag-seintje (ADR-023, “alleen een link”) — stond hard in de code (AcsUitslagNotificaties, onderwerp + PlainText-body als inline strings). Elke tekstuele wijziging (toon, taalfout, extra zin) vereiste een code-aanpassing + volledige deploy — onwerkbaar voor de beheerder/opdrachtgever, en het schaalt slecht zodra er meer mailtypes bijkomen (bv. wachtwoordherstel — het Email-veld daarvoor bestaat al in het domein).

Beslissing. De e-mailteksten worden beheerbare stamdata, in lijn met de bestaande Keuzelijst/Systeeminstellingen-aanpak, via een eigen aggregate EmailSjabloon (i.p.v. een Keuzelijst-rij) omdat een mail twee velden heeft — onderwerp én body — die de platte sleutel/omschrijving-structuur niet netjes vangt.

  1. Mailtype als sleutel. Elk sjabloon heeft een vaste sleutel = het mailtype (bv. uitslag-klaar), per taal. Zo hangt er een toekomstvaste structuur: een nieuw mailtype = een seed-rij + een placeholder-definitie, zónder de verzendcode te herschrijven.
  2. Placeholder-contract. Per mailtype ligt in code vast welke placeholders zijn toegestaan (voor uitslag-klaar: uitsluitend {link}). Bij het opslaan wordt gevalideerd dat onderwerp + body geen onbekende {…} bevatten — zo lekt er niets door en kan een beheerder geen niet-ingevulde placeholder introduceren.
  3. Fail-graceful met code-defaults. De standaardtekst (de huidige hardcoded tekst) staat als definitie in code en wordt geseed. Kan de verzendende poort de beheerde rij niet lezen (geen rij, database onbereikbaar), dan valt ze terug op die code-default — de mail blijft dus altijd verzendbaar, consistent met de fail-graceful lijn van ADR-023. De singleton-poort AcsUitslagNotificaties opent per verzending een service-scope om het sjabloon uit de (scoped) AppDbContext te lezen.
  4. ADR-023 blijft leidend. Het uitslag-seintje mag geen inhoudelijke uitslag bevatten, alleen een link; de beheer-UI toont die regel expliciet als waarschuwing zodat een beheerder haar niet per ongeluk doorbreekt. Alleen platte tekst (de huidige mail is PlainText); HTML-body is out-of-scope.

Status. Aanvaard (2026-07-13c) — modelwijziging (nieuwe aggregate EmailSjabloon in de Beheer-context).

Gevolgen.

AspectGevolg
Beheer zonder deployOnderwerp + body van het uitslag-seintje zijn aanpasbaar in het beheerportaal (tab E-mailteksten); geen code-wijziging of deploy meer nodig voor een tekstwijziging.
ToekomstvastLatere mails (wachtwoordherstel e.d.) gebruiken hetzelfde mechanisme — nieuw mailtype = seed-rij + placeholder-definitie.
RobuustheidZonder beheerde rij of bij een DB-storing valt de app terug op de code-default; het seintje blijft werken.
Privacy/anonimiteitOngewijzigd t.o.v. ADR-023: geen persoonsgegevens of scores in de tekst; de UI waarschuwt en het placeholder-contract laat alleen {link} toe.

Zie Systeembeheer → E-mailteksten en de term E-mailsjabloon.


ADR-043 — Eigen beoordelingscriteria: open vragen op de smiley-schaal

Section titled “ADR-043 — Eigen beoordelingscriteria: open vragen op de smiley-schaal”

Context. Issue #20: naast de vaste beoordelings-secties/-attributen (gezicht/kleding/accessoires, beheerbare stamdata) wil een maker soms een eigen, vrij geformuleerd criterium kunnen laten scoren — bijvoorbeeld “past deze bril bij mijn gezicht?”. De functie moet simpel en optioneel zijn; de hulptekst en het maximale aantal moeten via het beheerportaal instelbaar zijn. Belangrijk: een eerdere variant met open antwoorden (vrije tekst per open vraag) is in 2026-07-04d bewust geschrapt ten gunste van één reactie per opiniegever — die keuze blijft staan.

Beslissing. (Verfijnd 2026-07-14a: twee niveaus — categorie + sub-vragen.) Een maker kan bij het stellen tot maxOpenVragen (nieuwe Systeeminstelling, standaard 3; 0 = uit) eigen open-vragen-categorieën toevoegen. Elke categorie heeft een titel (kop) en daaronder tot maxOpenSubvragen (standaard 5) eigen sub-vragen; de opiniegevers scoren elke sub-vraag op dezelfde smiley-schaal 1–5 als een gewoon attribuut — dus scores, geen vrije tekst (géén herleving van de geschrapte open antwoorden). De categorie-kop zelf wordt niet gescoord — precies zoals de vaste secties (Gezicht → ogen/mond/…). (De eerste versie (2026-07-13d) modelleerde een open vraag als één losse score; testfeedback vroeg om de categorie-met-sub-vragen-structuur.)

  1. Modelleren als synthetische sectie per categorie. De categorieën worden op de Vraag bewaard als lijst OpenVraag ({ sleutel, titel, subvragen }) met sub-items OpenSubvraag ({ sleutel, tekst }). Elke categorie wordt tegelijk als een eigen sectie open:<sleutel> (met de sub-vraag-sleutels als attributen) aan de Beoordelingsselectie toegevoegd. Zo werken de bestaande verplichte-scoring-validatie, de slot-/verdeel-logica, het AI-vangnet en de aggregatie ongewijzigd; alleen de labels (categorie-titel + sub-vraag-tekst) komen uit openVragen i.p.v. de Keuzelijst. Dit vermijdt een parallelle antwoord-opslag en dus extra migratie op Opinie.
  2. Beheerbaar. De maxima zijn maxOpenVragen (categorieën) en maxOpenSubvragen (sub-vragen per categorie); de hulptekst voor de maker is beheerbare content (Keuzelijst-type uitleg, sleutel open-vragen) — alle zonder code-wijziging aanpasbaar, consistent met issue #20.
  3. Moderatie. Zowel de categorie-titel als de sub-vraag-teksten zijn door de maker ingetypte vrije tekst en lopen daarom vóór opslag door de tekstmoderatie (zoals de toelichting).

Status. Aanvaard (2026-07-13d; verfijnd 2026-07-14a) — modelwijziging (VO OpenVraag = categorie + OpenSubvraag-lijst op Vraag.openVragen, instellingen maxOpenVragen + maxOpenSubvragen).

Gevolgen.

AspectGevolg
Minimale impactHergebruik van de bestaande Scores/sectie-machinerie; alleen een nieuw jsonb-veld op Vraag (+ migratie) en labelweergave.
Optioneel & simpelZonder open vragen verandert er niets; met maxOpenVragen = 0 is de functie volledig uit.
BeheerbaarAantal (instelling) en hulptekst (uitleg/open-vragen) zijn zonder deploy aanpasbaar.
ConsistentieOpen vragen worden gescoord, niet in vrije tekst beantwoord — de 2026-07-04d-keuze (één reactie per opiniegever) blijft intact.

Zie Vraag stellen, Opinie geven en de term Open vraag.


ADR-044 — Meerdere omgevingen in één resourcegroep, met persistente DNS-zones en custom domains

Section titled “ADR-044 — Meerdere omgevingen in één resourcegroep, met persistente DNS-zones en custom domains”

Context. ADR-039 deel 2 stelde een aparte resourcegroep per omgeving voor (Clermond-Test, Clermond-Preprod), maar was nog niet aanvaard. De opdrachtgever heeft nu concreet gekozen: een verse productie-omgeving en een testing-omgeving, beide in één nieuwe resourcegroep Clermond-2, elk met een eigen custom domein (productie advice-4-me.com, testing advies-4-mij.nl, registrar Vimexx) en per branch gedeployed. Belangrijke eis tijdens de ontwikkeling: omgevingen moeten goedkoop up/down te halen zijn (pulumi destroy/up) — maar de DNS-delegatie bij de registrar mag daar nooit door breken. De oude, handmatig opgezette productie in resourcegroep Clermond blijft ongemoeid staan tot ze handmatig wordt opgeruimd.

Beslissing.

  1. Eén resourcegroep, omgevingslabel in elke resourcenaam. Het bestaande slug-mechanisme (infra/config.ts) geeft alle resources van een omgeving een -<slug>-suffix (iseeyou-api-prod, pg-iseeyou-test, …). Stacks: productie (slug prod) en testing (slug test), beide in Clermond-2. Dit vervangt het “aparte resourcegroep per omgeving”-voorstel: het label maakt omgevingen net zo onderscheidbaar, en een pulumi destroy verwijdert uitsluitend de eigen, gelabelde resources.
  2. Persistente resources in een apart Pulumi-project infra/dns/ (project iseeyou-dns, stacks productie/testing): per domein de Azure DNS-zone én het ACS-e-maildomein (EmailService + CustomerManaged Domain + SenderUsername noreply, met DKIM/SPF/verificatie-records in de zone). Een eigen project met eigen state is de enige vorm waarbij een omgevings-destroy de zone en de DKIM-sleutels aantoonbaar niet kán raken; protect: true en een workflow zónder destroy-actie zijn de tweede vangrail. De nameservers worden éénmalig bij Vimexx gedelegeerd.
  3. Omgevingsgebonden DNS-records in het omgevingsproject. De env-stack schrijft records in de bestaande zone (apex-ALIAS + www/beheer/docs-CNAME’s naar de Static Web Apps, api-CNAME + asuid-TXT naar de App Service) en gaat met de omgeving mee op/neer. Custom domains per omgeving: apex + www → gebruikersportaal, beheer. → beheerportaal, api. → API (gratis managed certificate), docs. → documentatie, afzender noreply@<domein>. Pulumi zet voortaan óók App__PortaalUrl en Cors__Origins (afgeleid van het domein) via een expliciete WebAppApplicationSettings-resource — de omgeving is daarmee in één up volledig werkend, zonder handmatige na-configuratie.
  4. Branch-per-omgeving. main deployt continu naar testing; promotie naar productie = main mergen in de branch productie, die naar de productie-omgeving deployt. De ci.yml-deploy-jobs zijn geparametriseerd op branch → omgeving (resourcenamen, VITE_API, health-URL).

Status. Aanvaard (2026-07-14b) — implementatie-/infrastructuurkeuze, geen modelwijziging. Vervangt deel 2 van ADR-039.

Gevolgen.

AspectGevolg
Up/down zonder DNS-schadepulumi destroy van een omgeving raakt de zone, de nameserver-delegatie en de DKIM-sleutels niet; een latere up maakt de omgeving compleet opnieuw, inclusief records en certificaten — Vimexx hoeft nooit meer aangeraakt.
KostenElke draaiende omgeving kost ±€35–40/mnd (App Service B1 + PostgreSQL B1ms + ACR Basic + docs-SWA Standard); een neergehaalde omgeving alleen nog de DNS-zone (±€0,50/mnd).
ReproduceerbaarheidApp-settings (incl. App__PortaalUrl, Cors__Origins, registry-credentials) worden bij elke up afgedwongen; handmatige setting-wijzigingen buiten Pulumi om overleven een up bewust niet.
E-mailMails komen van noreply@<domein> i.p.v. DoNotReply@<guid>.azurecomm.net; SPF/DKIM staan in de persistente zone, verificatie is een éénmalige stap na de delegatie.
Oude productieBlijft in Clermond draaien (geen datamigratie); opruimen is een aparte, handmatige stap van de opdrachtgever.

Zie Deployment — Meerdere omgevingen en infra/README.md.


Het oude prototype (Node/Express met JSON-bestanden, in JavaScript) dient uitsluitend als referentie/uitgangspunt. De greenfield-doelarchitectuur wijkt op de volgende punten bewust af:

OnderwerpPrototype (referentie)Greenfield-doelarchitectuurADR
Backend-platformNode / Express (JavaScript).NET / C# (typeveilig)ADR-001
PersistentieJSON-bestanden + in-memory cachePostgreSQL + EF Core (Npgsql)ADR-002
StructuurVlakke server.jsGelaagd + DDD, modulaire monoliet per contextADR-003
Use-casesAd-hoc in routesDunne, expliciete handlers (geen MediatR)ADR-004
DatalevensduurJSON-stores, ad-hoc opruimenVluchtig + persistent in één DB, retentie via BackgroundServiceADR-005
RealtimeNiets / pollingSignalR voor push, REST voor mutaties; Redis-backplane bij schaalADR-006
WachtwoordenPlatte tekstArgon2id (gebruikers) + ASP.NET Core Identity (beheer)ADR-007
Media-opslagLokaal filesystemIMediaStore: lokaal/MinIO ↔ S3-compatibel, bytes in object-storageADR-008
Foto vs. videoVideo-onlyEén media-pipeline via MediaType { Video, Foto }ADR-009
RetentieUitschakelbare ENABLE_CLEANUP-flagRetentie altijd aan (RetentieHostedService); verwijderen zodra de beoordeling klaar is, met instelbare veiligheids-timeoutADR-010
Ster-toekenningIn applicatielogicaAfdwingbaar via unieke index op Ster(vraag_id)ADR-002
LocatieVrij nationaliteit-veld + handmatige NL-regiolijstGeen Land-veld (single-tenant per site, NL eerst); Regio als vlakke keuzelijstADR-037
Aantallen & termijnenVaste getallen (10 opiniegevers, 30 min, …)Via Beheer instelbaar (Systeeminstellingen)ADR-017
Te weinig kandidatenMinder of geen opiniesOpnieuw toewijzen + AI-persona-vangnetADR-017
Beoordeelde veldenDrie vaste sectiesBeoordelingsselectie door de maker (standaard Gezicht/Kleding/Accessoires)ADR-017
Deelname-poortn.v.t.Credits als enige deelname-poort: beoordelen verdient, vragen kost; te weinig → eerst verdienen (vervangt reciprociteit en “eerst zelf beoordelen”)ADR-017
Credits & waarderingn.v.t.Credits = deelname-poort; waardering = 0–5 kwaliteitsoordeel (smiley-schaal) dat de maker aan elke ontvangen beoordeling geeft (geen loterij; per 2026-07-05 weegt ze mee in de ster-ranking en komt er via ADR-027–029 weer geld — gescheiden van de credits)ADR-019
Wachtwoordhersteln.v.t.Optioneel e-mailadres, uitsluitend voor herstelADR-007
Uitslagnotificatien.v.t.Optioneel per vraag (notificatieEmail): één mail met alleen een link via ACS Email, fail-graceful, adres na verzending gewistADR-023
AI-persona-invullingn.v.t. (open punt)Vision-gebaseerde IAiPersonaService op OpenAI gpt-4o-mini (foto direct, video via FFmpeg-frames), fail-graceful met deterministische terugval; AI-opinie ook handmatig door de beheerderADR-024
Vrije reactieAltijd optioneelMaker bepaalt met VrijeTekstGevraagd of er een vrije reactie gevraagd wordt (dan verplicht); alleen-vrije-tekst-vraag (zonder scores) magADR-025
AI-reactie & toelichtingn.v.t.AI-persona geeft scores + een onderbouwde reactie en krijgt de toelichting mee (herkaderde, opt-in prompt)ADR-025
Aantal & herkomst & waarderingVast (10 opiniegevers); ster-toekenning in codeStandaard 10 beoordelingen (aantalOpiniegevers, instelbaar; kort 1 in 2026-07-04h, per 2026-07-05 terug naar 10); herkomst (mens/AI) verborgen voor de maker (wel in beheer); elke ontvangen beoordeling waardeerbaar (AI-waardering raakt geen reviewer-profiel)ADR-026 / ADR-030
Sterren & uitbetalingLoterij-ster + uitbetaling in applicatielogica(Voorgesteld 2026-07-05) Verdiende ster via de consensus-ranking + mijlpalen; uitbetaling terug via PSP (KYC), mechaniek deels openADR-027
Betaald adviesn.v.t.(Voorgesteld 2026-07-05) Adviesniveaus 1–4/Expert met verplichte tekst en prijzen per beoordeling; geblurde teasers met ontgrendeling per beoordelingADR-028
Geld & saldon.v.t.(Voorgesteld 2026-07-05) Euro-saldo via iDeal/PSP met eenmalige welkomstbonus; strikt gescheiden van creditsADR-029
Onboardingn.v.t.(Voorgesteld 2026-07-05) Welkomsttekst, eerst-2-beoordelen-poort met cold-start-vangrail, tips-vensters; default 10 beoordelingenADR-030
FrontendPrototype-React in JavaScriptGreenfield React 19 + Vite + TypeScript (monorepo)ADR-012
Portalen / APIn.v.t.Twee portalen op één API via autorisatiebeleidADR-013
Hostingn.v.t.Cloud-agnostisch, Docker + scale-to-zeroADR-015

Zie het Domeinmodel, de Termen en de overige Architectuur-pagina’s voor de uitwerking.