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.
Overzicht
Section titled “Overzicht”| ADR | Titel | Status |
|---|---|---|
| ADR-001 | .NET / C# als platform | Aanvaard |
| ADR-002 | PostgreSQL + EF Core als persistentie | Aanvaard (vervangt JSON + cache) |
| ADR-003 | Gelaagd + DDD, modulaire monoliet | Aanvaard |
| ADR-004 | Geen MediatR | Aanvaard |
| ADR-005 | Vluchtig + persistent in dezelfde DB | Aanvaard |
| ADR-006 | SignalR voor push, REST voor mutaties | Aanvaard |
| ADR-007 | Geen Keycloak; Identity + anonieme auth | Aanvaard |
| ADR-008 | IMediaStore, cloud-agnostische opslag | Aanvaard |
| ADR-009 | Foto en video in één media-pipeline | Aanvaard |
| ADR-010 | Retentie altijd actief | Aanvaard |
| ADR-011 | Single-device-sessies | Aanvaard |
| ADR-012 | React 19 + Vite + TypeScript | Aanvaard |
| ADR-013 | Twee portalen op één API | Aanvaard |
| ADR-014 | Single-tenant | Aanvaard |
| ADR-015 | Cloud-agnostische hosting, scale-to-zero | Aanvaard |
| ADR-016 | Land als keuzelijst (ISO 3166-1) + afgeleide UN M49-regio | Aanvaard (vervangt GeoIP-locatie) |
| ADR-017 | Credits als deelname-poort, instelbare parameters, online als voorrang, AI-persona-vangnet | Aanvaard (credits-poort vervangt reciprociteit) |
| ADR-018 | Credits ≠ sterren (gescheiden valuta) | Ingetrokken (sterren geschrapt, zie ADR-019) |
| ADR-019 | 0–5 kwaliteitswaardering vervangt loterij-ster + uitbetaling | Aanvaard (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-027–ADR-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-020 | OpenAI-tekstmoderatie (gratis, fail-open) | Aanvaard |
| ADR-021 | Open vragen/antwoorden i.p.v. een algemeen opmerkingenveld | Aanvaard, 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-022 | Instelbaar aantal beoordelingen met schalende kosten | Aanvaard, 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-023 | Uitslagnotificatie per e-mail: alleen een link, via Azure Communication Services | Aanvaard (2026-07-04e) |
| ADR-024 | Vision-gebaseerde AI-persona op gpt-4o-mini + handmatige AI-opinie | Aanvaard (2026-07-04f) |
| ADR-025 | Maker bepaalt de vrije reactie (verplicht wanneer gevraagd); AI geeft nu een reactie | Aanvaard (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-026 | Standaard 1 beoordeling; AI-herkomst verborgen voor de maker; elke beoordeling waardeerbaar | Aanvaard (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-027 | Sterren als verdiende kwaliteitsreputatie via de consensus-ranking | Vastgesteld (2026-07-08b, geverifieerd door Han; geïmplementeerd 2026-07-08) |
| ADR-028 | Betaald advies met adviesniveaus en de teaser-flow | Vastgesteld (2026-07-08b, geverifieerd door Han; geïmplementeerd 2026-07-08) |
| ADR-029 | Euro-saldo met iDeal (via een PSP) en eenmalige welkomstbonus — geld keert terug | Vastgesteld (2026-07-08b, geverifieerd door Han; overrulet het “geen geld”-deel van ADR-019) |
| ADR-030 | Onboarding met eerst-2-beoordelen-poort (met vangrail) en default terug naar 10 | Vastgesteld (2026-07-08b, geverifieerd door Han; geïmplementeerd 2026-07-08) |
| ADR-031 | In-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-032 | Observability: Serilog-gestructureerde logging + Application Insights | Aanvaard (2026-07-11) |
| ADR-033 | CI/CD op een self-hosted runner (build, test én automatische deploy naar Azure) | Aanvaard (2026-07-11) |
| ADR-034 | Visuele moderatie van opnames vóór distributie (fail-open) | Aanvaard (2026-07-11g) |
| ADR-035 | Mollie als PSP-implementatie; betaalpoort async + status-opvraag | Aanvaard (2026-07-11h) |
| ADR-036 | First-party product-analytics (cookieloos, zonder PII, geen externe dienst) | Aanvaard (2026-07-11) |
| ADR-037 | Land vervalt (single-tenant per site); Regio wordt een vlakke keuzelijst | Aanvaard (2026-07-11m) |
| ADR-038 | Audittrail voor beheer-support (mét actor-identiteit, admin-only) | Aanvaard (2026-07-11n) |
| ADR-039 | Infrastructure 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-040 | Login-normalisatie (getrimd + hoofdletter-ongevoelig) en uniek e-mailadres | Aanvaard (2026-07-13) |
| ADR-041 | Client-side crash-rapportage (first-party, cookieloos) + auto-reload bij verouderde chunks | Aanvaard (2026-07-13) |
| ADR-042 | Beheerbare e-mailteksten per mailtype, met placeholder-contract | Aanvaard (2026-07-13c) |
| ADR-043 | Eigen beoordelingscriteria: open vragen op de smiley-schaal | Aanvaard (2026-07-13d) |
| ADR-044 | Meerdere omgevingen in één resourcegroep, met persistente DNS-zones en custom domains | Aanvaard (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.
Beslissingen gekoppeld aan drijfveren
Section titled “Beslissingen gekoppeld aan drijfveren”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)"]
ADR-001 — .NET / C# als platform
Section titled “ADR-001 — .NET / C# als platform”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.
| Aspect | Gevolg |
|---|---|
| Voordeel | Sterke typeveiligheid: domeininvarianten en value objects worden door de compiler bewaakt i.p.v. door losse runtime-checks. |
| Voordeel | Eén ecosysteem dekt API (Minimal API), realtime (SignalR), achtergrondtaken (BackgroundService) en data (EF Core) zonder losse stukjes te lijmen. |
| Gevolg | De prototype-JS wordt niet hergebruikt; het dient als functionele referentie, niet als codebasis. |
| Beperking | Het 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.
| Aspect | Gevolg |
|---|---|
| Voordeel | Echte 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. |
| Voordeel | Horizontaal schaalbaar: meerdere stateless API-instances delen één database (vereiste voor ADR-013/ADR-015). |
| Voordeel | jsonb voor de 28 scores houdt het schema compact zonder 28 kolommen of join-tabellen. |
| Beperking | Vereist een (managed) PostgreSQL-instantie; meer operationele zorg dan losse JSON-bestanden, maar onmisbaar voor schaal en integriteit. |
| Gevolg | Mediabytes 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.
| Aspect | Gevolg |
|---|---|
| Voordeel | Heldere modulegrenzen per context, maar één deployable: lage operationele kosten, geen netwerk-overhead tussen contexten. |
| Voordeel | De afhankelijkheidsrichting houdt het domein puur en testbaar; poorten (interfaces) maken Infrastructure verwisselbaar. |
| Voordeel | Stateless ontworpen, klaar voor horizontale schaal (ADR-006/ADR-015). |
| Beperking | Modulegrenzen moeten bewust bewaakt worden; een monoliet maakt het verleidelijk om grenzen te overschrijden. |
| Uitbouw | Een 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.
| Aspect | Gevolg |
|---|---|
| Voordeel | Aanroep is expliciet en goed te volgen (geen “magische” dispatch via reflectie). |
| Voordeel | Eén afhankelijkheid minder; minder ceremonie voor een project van deze omvang. |
| Beperking | Cross-cutting gedrag (logging, validatie) wordt expliciet toegevoegd i.p.v. via mediator-pipelines; voor dit project een acceptabele afweging. |
| Conventie | Eé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.
| Aspect | Gevolg |
|---|---|
| Voordeel | Eén datastore: lagere kosten en minder operationele overhead dan een aparte vluchtige store. |
| Voordeel | Timestamp-indexen maken het periodiek opruimen goedkoop en voorspelbaar. |
| Anonimiteit | Vluchtige data verdwijnt structureel; het opruimen is eersteklas gedrag, geen bijzaak. |
| Beperking | Vluchtige en persistente tabellen delen één DB-budget; indexen en opruiminterval moeten op het volume afgestemd blijven. |
| Gevolg | Mediabytes vallen hier buiten: die staan in object-storage en worden door dezelfde retentie meegenomen (ADR-008/ADR-010). |
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.
| Aspect | Gevolg |
|---|---|
| Voordeel | Heldere scheiding: REST is de bron van waarheid voor mutaties; SignalR is puur notificatie. Goed testbaar. |
| Voordeel | Schaal-klaar: zonder socket-state kunnen instances vrij bij- en afschalen; Redis verbindt ze pas wanneer nodig. |
| Verbetering | Het prototype had geen push (polling/niets); realtime maakt presence en de opinie-flow direct. |
| Beperking | Twee kanalen (REST + WebSocket) betekent iets meer infrastructuur dan louter REST; de winst in directheid weegt op. |
| Config | Eé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 eenemailopgeven — 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.
| Aspect | Gevolg |
|---|---|
| Lage kosten | Geen aparte identity-server om te hosten, te patchen en te beveiligen; alles draait in dezelfde API. |
| Anonimiteit | Gebruikers hebben standaard geen persoonsgegeven nodig; een e-mailadres is optioneel en wordt alleen voor wachtwoordherstel gebruikt. |
| Beveiliging | Argon2id-hashing voor gebruikers; Identity verzorgt veilige hashing en optionele MFA voor beheerders. |
| Beperking | Herstel 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. |
| Gevolg | Twee auth-mechanismen in één API; de scheiding zit in autorisatiebeleid, niet in aparte deployables. |
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.
| Aspect | Gevolg |
|---|---|
| Lage kosten | Lokaal/dev draait op filesystem of MinIO zonder cloudkosten; productie gebruikt goedkope object-storage. |
| Cloud-agnostisch | Eén S3-compatibele implementatie werkt bij meerdere providers; geen lock-in, wisselen via configuratie. |
| Voordeel | Mediabytes belasten de database niet; de DB blijft klein en goedkoop te schalen. |
| Implementatie | Range-streaming voor video; foto en video delen dezelfde opslag-pipeline (ADR-009). |
| Retentie | Verlopen 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.
| Laag | Gevolg |
|---|---|
| Capture | De camera-hook (ADR-012) ondersteunt twee opname-modi via getUserMedia/MediaRecorder: video opnemen óf één foto vastleggen. |
| Opslag | Het bestand kan video of afbeelding zijn; mediaType + key/metadata leggen vast welk type het is. |
| Weergave | De 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. |
| Model | Eén aggregate, één matching- en opinie-flow; minder duplicatie en consistente sectiegemiddelden. |
| Retentie | Foto’s vallen onder hetzelfde retentiebeleid (ADR-010): altijd verwijderen zodra de beoordeling klaar is. |
Zie het scenario Vraag stellen met foto en Media & opname.
ADR-010 — Retentie altijd actief
Section titled “ADR-010 — Retentie altijd actief”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.
| Aspect | Gevolg |
|---|---|
| Anonimiteit | Concrete, 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. |
| Verbetering | Geen ENABLE_CLEANUP-flag meer; opruimen is structureel en niet uit te zetten. |
| Voordeel | Beperkt opslag en aansprakelijkheid; opruimen is eersteklas gedrag. |
| Beperking | Vragen 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. |
| Implementatie | De taak verwijdert media + DB-record + vluchtige bijhorende data in één opruimronde. |
ADR-011 — Single-device-sessies
Section titled “ADR-011 — Single-device-sessies”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.
| Aspect | Gevolg |
|---|---|
| Anonimiteit | Beperkt parallel gebruik en daarmee misbruik-oppervlak. |
| Voordeel | Eenduidig online-begrip per gebruiker; eenvoudiger matching en minder dubbeltellingen. |
| Beperking | Een gebruiker kan niet gelijktijdig op twee apparaten ingelogd zijn; inloggen elders logt het eerste apparaat uit. |
| Implementatie | Per gebruiker wordt één actieve sessionId bewaard; bij login wordt een bestaande sessie ingetrokken. |
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.
| Aspect | Gevolg |
|---|---|
| Camera-centrisch | Directe, breed beschikbare toegang tot getUserMedia/MediaRecorder; de camera-hook is getypeerd en deelbaar. |
| Voordeel | Breed ecosysteem dekt alle behoeften (camera, server-state, formulieren/validatie, i18n, realtime) met volwassen libs. |
| Voordeel | TypeScript + een uit OpenAPI gegenereerde client geven end-to-end typeveiligheid tussen frontend en API. |
| Voordeel | Monorepo deelt ui/api-client/shared tussen beide portalen zonder duplicatie. |
| Beperking | Geen volledige .NET-end-to-end (zoals Blazor zou geven); de OpenAPI-client overbrugt dat met gegenereerde types. |
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.
| Aspect | Gevolg |
|---|---|
| Lage kosten | Eén API om te hosten, te bouwen en te bewaken in plaats van twee. |
| Schaal | Eén stateless deployable schaalt horizontaal; beide portalen profiteren van dezelfde scale-out (ADR-015). |
| Voordeel | Policies bepalen toegang fijnmazig; portaalgrenzen vallen niet samen met context-grenzen, wat flexibel is. |
| Beperking | De autorisatiebeleidslogica moet zorgvuldig zijn: één API bedient zowel anonieme gebruikers als beheerders. |
| Conventie | Endpoints krijgen expliciet een policy; standaard is dichtgezet (deny-by-default). |
Zie Architectuur — Backend & API en Frontend.
ADR-014 — Single-tenant
Section titled “ADR-014 — Single-tenant”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.
| Aspect | Gevolg |
|---|---|
| Lage kosten | Geen multi-tenant-complexiteit (tenant-kolommen, row-level isolatie, routing); eenvoudiger te bouwen en te bewaken. |
| Voordeel | Eenvoudiger datamodel en query’s; minder kans op tenant-lek-fouten. |
| Beperking | Een tweede, geïsoleerde omgeving zou een aparte deployment/DB vereisen. |
| Herziening | Mocht 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.
| Aspect | Gevolg |
|---|---|
| Lage kosten | Scale-to-zero betekent (bijna) geen compute-kosten bij weinig verkeer. |
| Schaal | Autoscaling vangt pieken op; de stateless API schaalt horizontaal, met Redis-backplane voor realtime. |
| Cloud-agnostisch | Eén Docker-image draait op meerdere platforms; managed Postgres + S3-compatibele storage vermijden lock-in. |
| Beperking | Scale-to-zero kan een koude-start geven na inactiviteit; acceptabel voor het verwachte gebruikspatroon. |
| Dev | Docker 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: hetLand-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 (
codealpha-2 + gelokaliseerdenaam) 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
genderen 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.
| Aspect | Gevolg |
|---|---|
| Schaal | Werkt voor álle landen via wereldwijde standaarden; geen handmatige per-land regiolijst meer te onderhouden. |
| Voordeel | Eén bewuste keuze uit een gelokaliseerde landenlijst; matching kan verbreden van land → subregio → werelddeel via de afgeleide UN M49-regio. |
| Anonimiteit | Geen 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 kosten | Geen GeoIP-dataset/-library te onderhouden of te betalen; de landenlijst is reguliere stamdata in de Keuzelijst. |
| Beperking | De 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. |
| Implementatie | Land 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 reciprociteitsdrempel 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 instellingaantalOpiniegevers— 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 zowelaantalEerstZelfBeoordelenalsreciprociteitDrempel: beide zijn verwijderd, en daarmee vervallen ook deGeblokkeerd-OpdrachtStatus enVraag.reciprociteitDoel. Slots ontstaan voortaan altijdOpen. 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 insturen → uitslag 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
SelectieCriteriaen 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
Openen 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.
| Aspect | Gevolg |
|---|---|
| Deelname | Eé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. |
| Vereenvoudiging | Met het schrappen van Geblokkeerd en reciprociteitDoel ontstaan slots altijd Open; geen half-in-de-pool hangende slots, en de Opdracht-toestandsmachine wordt eenvoudiger. |
| Schaal | Het platform past zich aan het verkeer aan: aantallen, termijnen en credit-bedragen worden afgestemd zonder code- of modelwijziging. |
| Robuustheid | Randgevallen “te weinig of geen kandidaten” worden door de AI-persona en het opnieuw toewijzen opgevangen, in plaats van met minder of geen opinies. |
| Anonimiteit | De 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. |
| Voordeel | Online-voorrang houdt de matching snel zonder offline kandidaten hard uit te sluiten. |
| Beperking | Instelbare parameters (incl. credit-bedragen) vragen om verstandige standaardwaarden en bewaking; de AI-persona-invulling moet nog uitgewerkt worden. |
| Implementatie | De 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 — per2026-07-05, ter verificatie: de ster keert terug als verdiende reputatie — géén loterij — en er komt weer geld in het systeem via ADR-027–ADR-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:
| Aspect | Gevolg (vervallen) |
|---|---|
credits 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.
ADR-023 — Uitslagnotificatie per e-mail: alleen een link, via Azure Communication Services
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-veldNotificatieEmailbijPOST /api/vragenkrijgt 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 (afzenderDoNotReply@…azurecomm.net), via de app-settingsEmail__ConnectionString,Email__AfzenderenApp__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
notificatieEmailopnullgezet (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.
| Aspect | Gevolg |
|---|---|
| Gebruikswaarde | De maker hoeft niet zelf te blijven checken; hij krijgt een seintje en keert via de link terug naar het portaal. |
| Anonimiteit | Geen beoordelingsinhoud in e-mail; het adres wordt eenmalig gebruikt en direct gewist — geen mailinglijst, geen herleidbaar spoor. Los van het herstel-adres. |
| Lage kosten | ACS Email is een lichte, pay-per-use dienst; geen eigen mailserver te beheren. |
| Robuustheid | Fail-graceful: aflever-/configuratieproblemen raken de kernfunctionaliteit niet; de uitslag blijft in de app leidend. |
| Beperking | Eén afzenderdomein/-configuratie (ACS) nodig in productie; zonder configuratie vervalt de notificatie stilzwijgend (no-op). |
| Implementatie | De 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:
IAiPersonaServiceop OpenAIgpt-4o-mini(vision). De opinie-generatie loopt via de applicatie-poortIAiPersonaService, met een standaard-adapter op OpenAIgpt-4o-mini(een goedkoop vision-model). De persona-instructiestuurt de “persoonlijkheid”; het model scoort alleen de door de maker gekozen secties/attributen — destijds op de schaal 1–5; per2026-07-05is 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 eenOpenslot laten genereren met een gekozen persona. 404/400 als er geen open slot is. Vult alleenOpenslots — nooit een door een mens vastgehouden slot. - Eén sleutel, extra permissie. De adapter gebruikt dezelfde app-setting
OpenAI__ApiKeyals 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.
| Aspect | Gevolg |
|---|---|
| Geloofwaardigheid | De AI-opinie slaat nu op de werkelijke opname (vision), niet op een blinde regel; het vangnet levert relevantere scores. |
| Bestuurbaarheid | De beheerder kan een trage/vastgelopen vraag direct afvullen zonder op de deadline te wachten, met een zelfgekozen persona. |
| Lage kosten | gpt-4o-mini is een goedkoop vision-model; met 1 beeld (foto) of ~4 frames (video) blijft een AI-opinie onder ~$0,01. |
| Robuustheid | Fail-graceful terugval op deterministische scores: zonder sleutel/permissie of bij een fout werkt de app en het vangnet gewoon door. |
| Anonimiteit | De 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. |
| Beperking | Vereist FFmpeg in het API-image en een OpenAI-sleutel met chat/completions-permissie in productie; zonder die permissie draait alleen de deterministische terugval. |
| Implementatie | Handmatige 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 vlagVrijeTekstGevraagdbestaat 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.maxVrijeReactiesZonderBetaaldper vraag zonder betaald advies; mét betaald advies zijn deniveaueisenleidend. 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.
Beoordelingsselectiekrijgt de vlagVrijeTekstGevraagd(bool). Een vraag is geldig zodra er ≥1 sectie is OFVrijeTekstGevraagd= true — dus alleen categorieën, alleen een vrije reactie, of beide. IsVrijeTekstGevraagd= true, dan is deOpinie.reactievoor 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/vragenkrijgtVrijeTekstGevraagdenSelectiemag leeg zijn;POST /api/opdrachten/{id}/opinieweigert een lege verplichte reactie. - AI-persona geeft nu een onderbouwde reactie + krijgt de toelichting. De poort
IAiPersonaServicekrijgt nu óók deVraag.toelichtingmee en geeft scores + een korte vrijereactieterug (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.
| Aspect | Gevolg |
|---|---|
| Gebruikswaarde | De 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. |
| Geloofwaardigheid | De 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. |
| Contentbeleid | De 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. |
| Robuustheid | De verplicht-regel zit in de use-case (niet als DB-constraint); de fallback levert alsnog een deterministische opinie met generieke reactie. |
| Beperking | De AI-reactie wordt bewust niet gemodereerd (modeltekst); dat leunt op de prompt-kadering en het model-contentbeleid. |
| Implementatie | Beoordelingsselectie.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 aantalBeoordelingen — ADR-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 beheerinstellingaantalOpiniegeversgaat 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 per2026-07-05teruggedraaid naar 10 — zie ADR-030.) - AI-herkomst verborgen voor de maker. Het veld
bron(mens/AI) is verwijderd uit de user-facing uitslag-responsGET /api/vragen/{id}/uitslag: de maker ziet de herkomst van een opinie niet meer. Het datamodel houdtOpinie.bron(de enumOpinieBron) onveranderd bij; de beheer-endpoints tonen de herkomst nog wél (zieGET /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}/waarderingwerkt 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.
| Aspect | Gevolg |
|---|---|
| Gebruikswaarde | Een lage default (aantalOpiniegevers = 1) maakt een vraag snel compleet en de eerste ervaring eenvoudig; wie meer beoordelingen wil, verhoogt de instelling. |
| Consistentie | De maker ziet geen herkomst meer én kan elke ontvangen beoordeling waarderen — als je AI niet kunt onderscheiden, moet je alles kunnen waarderen. |
| Zuivere statistieken | Een waardering op een AI-opinie raakt geen reviewer-profiel; de reviewer-statistieken blijven zuiver (alleen menselijke opinies tellen mee). |
| Anonimiteit/privacy | De maker krijgt geen signaal over de bron van zijn beoordelingen; de herkomst blijft intern (datamodel + beheer). |
| Beperking | Bij 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. |
| Implementatie | Default 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 in2026-07-08ben 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 vanafminBeoordelaarsVoorRanking(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.
| Aspect | Gevolg |
|---|---|
| Kwaliteit | Goede beoordelaars worden zichtbaar en blijvend beloond; de ranking straft willekeurig/extreem scoren af (correlatie-controle). |
| Samenhang | De bestaande waardering krijgt een tweede functie (wegingsfactor) zonder te veranderen voor de maker. |
| Bestuurbaarheid | Drempels/gewichten via Beheer; de formule zelf is een implementatiekeuze na verificatie. |
| Beperking | Ranking vereist ≥ 3 beoordelaars; bij kleinere vragen loopt alleen de credits-mijlpaal-route. Gelijkspel-afhandeling is een implementatiekeuze. |
| Implementatie | Server-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 in2026-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
Adviesniveauhebben (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: elkesterrenPerNiveau(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.
| Aspect | Gevolg |
|---|---|
| Verdienmodel | Goede beoordelaars verdienen echt geld; het platform krijgt een inkomstenstroom (verdeelsleutel 50/50). |
| Gratis route intact | Smiley-scores en de gratis (gevraagde) reactie veranderen niet; betaald advies is additief. |
| Conversie | De geblurde teaser toont elke vraagsteller wat er te koop is, zonder gratis weg te geven. |
| Beperking | Vereist voldoende niveau-beoordelaars (teaser is best effort) en een uitgewerkte niveau-toekenning (via sterren — te bevestigen). |
| Implementatie | Snapshots (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 in2026-07-08b. Welkomstbonus max €5 (totaal max €10), uitbetaling via het profielscherm vanaf €10. Overrulet het “geen geld in het systeem”-besluit van2026-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.
| Aspect | Gevolg |
|---|---|
| Lage drempel | iDeal is de NL-standaard; de welkomstbonus verlaagt de instap. |
| Compliance | Betalingen/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. |
| Anonimiteit | Houdbaar geherformuleerd (richting andere gebruikers); dataminimalisatie: alleen referentie + saldo in het platform. |
| Zuivere economie | De strikte scheiding houdt de gratis credits-cyclus onaangetast. |
| Beperking | De 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 beheerbareKeuzelijst-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
aantalOpiniegeversterug 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.
| Aspect | Gevolg |
|---|---|
| Wederkerigheid | Nieuwe gebruikers dragen aantoonbaar bij vóór ze consumeren, en verdienen precies genoeg voor hun eigen uitslag. |
| Geen deadlock | De vangrail voorkomt het kip-en-ei-probleem van de eerder afgeschafte poort. |
| Kwaliteit | Tips-vensters helpen (nieuwe) beoordelaars goed en eerlijk te scoren. |
| Beperking | De poort geldt alleen voor de eerste vraag; de vangrail moet betrouwbaar “geen werk beschikbaar” detecteren. |
| Implementatie | Instellingen 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/loginverifieert de admin-credentials uit hetSysteem-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 → anders401. Uitzonderingen die publiek blijven: de login zelf en de read-onlyGET /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 meegegevenactor-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.
| Aspect | Gevolg |
|---|---|
| Beveiliging | De gedemonstreerde exploits (creditfraude, PII-dump via de gebruikerslijst, config-tampering, willekeurig verwijderen) zijn dicht: beheer-endpoints geven zonder geldig admin-token 401. |
| Cross-origin | Een bearer-header werkt wél tussen de SWA-portalen en de App-Service-API; geen SameSite-beperking. |
| Beperking | Een 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 doel | Cookies (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). |
| Config | Auth: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 viaUseSerilogRequestLogging. Lokaal leesbare console-output; in productie compacte JSON. - Application Insights als APM:
AddApplicationInsightsTelemetryverzamelt requests, dependencies en exceptions; de Serilog-events gaan als traces mee via de Application Insights-sink. De koppeling loopt uitsluitend via de standaard app-settingAPPLICATIONINSIGHTS_CONNECTION_STRING— zonder 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.
| Aspect | Gevolg |
|---|---|
| Inzicht | Requests, fouten, afhankelijkheids-latency (OpenAI, ACS, Postgres) en de vision/vangnet-terugvallen zijn doorzoekbaar in Application Insights. |
| Fail-graceful zichtbaar | De “stille” terugvallen loggen nu gestructureerd (met properties i.p.v. tekstprefixen als [ai-persona]), dus er is op te alerten en te dashboarden. |
| Kosten | Application Insights rekent per GB ingest; bij pilot-volumes verwaarloosbaar. De sampling-standaarden van de SDK blijven aan. |
| Cloud-agnostisch | Serilog 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). |
| Privacy | Logs 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 viarun.cmd, gemonitord/herstart via dedeploy-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 naarmain(nooit bij een pull request) en alleen voor de onderdelen waarvan bestanden wijzigden (bepaald viagit difftussen 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 handmatigevN-reeks uit depak-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/instellingentot 200) vóórdat de job slaagt. - De handmatige
az-deploy-stappen in depak-issue-skill blijven bestaan als fallback/noodpad, niet als het normale pad.
Status. Aanvaard (2026-07-11) — implementatiekeuze, geen modelwijziging.
Gevolgen.
| Aspect | Gevolg |
|---|---|
| Betrouwbaarheid | Verificatie en deploy staan niet meer los van elkaar: wat groen is op main, staat (voor de gewijzigde onderdelen) ook automatisch live. |
| Beschikbaarheid van de pijplijn | De pijplijn is afhankelijk van één laptop (geen hoge beschikbaarheid); zonder draaiende runner blijft een push wachten tot iemand hem weer start. |
| Secrets | Geen Azure-credentials in GitHub-secrets — kleiner lek-oppervlak, maar wel een afhankelijkheid van de interactieve az login-sessie op die specifieke laptop. |
| Cloud-agnostisch | Onveranderd: 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 pad | De 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.
| Aspect | Gevolg |
|---|---|
| Veiligheid | Ongepaste opnames bereiken de opiniegevers niet meer; melden wordt tweede linie i.p.v. enige linie. |
| Privacy | De 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 & latentie | De Moderation API is gratis; het stellen van een vraag krijgt één extra API-call (foto) of een FFmpeg-run + call (video) — orde één seconde. |
| Beperking | Fail-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. |
| Testbaarheid | De 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 (zonderMollie:ApiKeywordt 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).IBetaalPoortkrijgt daarom een status-opvraag enStartwordt async (de Mollie-call is netwerk):StartAsync(...) → BetaalStart(PspReferentie, RedirectUrl)enIsGeslaagdAsync(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 endpointPOST /api/betalingen/psp-webhookleest hetid, vraagt de status op en roept de bestaande, idempotenteBetaalService.WebhookAsync(referentie, geslaagd)aan. De bestaande body-based/api/betalingen/webhookblijft voor de dummy. - Secrets:
Mollie:ApiKeyen de publieke API-basis-URL (App:ApiBaseUrl, voor dewebhookUrl) via Azure-config/Key Vault — zelfde patroon alsOpenAI__ApiKey. Het platform bewaart nog steeds alleen transactiereferentie + saldo. - Buiten scope (latere story): geautomatiseerde uitbetaling aan beoordelaars (een
IUitbetaalPoortmet 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.
| Aspect | Gevolg |
|---|---|
| Lage frictie | iDeal via Mollie is de NL-standaard: beste conversie, laagste kosten voor micro-bedragen, geen maandvast/lock-in. |
| Beveiliging | De uitkomst komt van een server-to-server statuscheck bij Mollie, niet van de (spoofbare) webhook-body; verwerking blijft idempotent op de referentie. |
| Terugval | Zonder Mollie:ApiKey blijft de dummy actief, dus lokaal/CI draaien zonder PSP-account; de flow (redirect → webhook) is identiek. |
| Inwisselbaar | Blijft achter de poort; een latere overstap naar Stripe/Adyen voor de marktplaats-/uitbetaalkant is een adapter, geen herontwerp. |
| Beperking | Uitbetaling/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/eventsmet een strikte allowlist van gebeurtenistypen (de funnel-stappen:portaal_geopend→welkom_gezien→poort_gestart→beoordeling_gegeven→vraag_gesteld→resultaat_ontgrendeld); onbekende typen worden geweigerd. - Geen PII, geen cookies: een gebeurtenis is
(type, meetId, tijdstip). DemeetIdis 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.
| Aspect | Gevolg |
|---|---|
| Privacy | Geen cookies, geen PII, geen data naar derden — geen consent-banner nodig en volledig in lijn met de anonimiteitsbelofte. |
| Kosten & beheer | Geen extern account of abonnement; de data staat in de bestaande database en valt onder de bestaande back-ups. |
| Beperking | Geen 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). |
| Misbruik | Het 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). |
| Funnel | De 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,SelectieCriteriaenPersonaProfielverliezen het veldLand. 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
landvervalt (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.
| Aspect | Gevolg |
|---|---|
| Eenvoud | Eén minder verplicht profielveld bij registratie; regiosVanLand-achtige land→regio-filtering in de UI vervalt. |
| Anonimiteit | Nog minder persoonsgegevens nodig — Land was al geen locatie-tracking, maar is nu ook geen los datapunt meer. |
| Data | Bestaande 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. |
| Internationalisatie | Uitbreiding 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, optioneeltarget/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 (per2026-07-13, ADR-040) mislukte login (MislukteLogin, met de reden indetails). Elke schrijfactie gaat mee in dezelfde databasetransactie als de business-mutatie die ze beschrijft (via de bestaandeIUnitOfWork, 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). EenAuditOpruimHostedService(zelfde patroon alsMeetOpruimHostedService) 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.
| Aspect | Gevolg |
|---|---|
| Support | De beheerder kan achteraf herleiden wie wat deed — essentieel bij klachten, betwiste transacties en misbruik-onderzoek. |
| Privacy | Bewuste, beperkte uitzondering op de anonimiteitsbelofte: identiteit is zichtbaar, maar uitsluitend voor de beheerder, nooit media, en met een eigen, begrensde retentie (niet oneindig). |
| Consistentie | Audit-writes gaan mee in dezelfde transactie als de mutatie — geen risico op een audittrail die uit de pas loopt met de werkelijke data. |
| Beheerlast | Eé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:
- 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 (geenpulumi up/importtegen de echte Azure-omgeving uitgevoerd; dat is een aparte, bewuste vervolgstap zodra de state-backend gekozen is). - 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 naartestbij elke push naarmain(zoals nu naar productie), draait daar de volledige Playwright-e2e-suite (nu alleen lokaal/handmatig via depak-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.
| Aspect | Gevolg |
|---|---|
| Herhaalbaarheid | De productie-infrastructuur staat nu in code (infra/) — reproduceerbaar, doorzoekbaar, en het uitgangspunt voor nieuwe omgevingen zodra die worden goedgekeurd. |
| Kosten | Deel 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 preprod | Een verplichte, groene e2e-run vóór promotie naar preproductie vangt regressies vroeger op dan de huidige “handmatig lokaal draaien”-aanpak. |
| Gedeelde resourcegroep | Nieuwe 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
AuditgebeurtenisMislukteLoginmet de reden (OnbekendeGebruiker/OnjuistWachtwoord) — admin-only zichtbaar in de Audittrail (ADR-038).
Status. Aanvaard (2026-07-13) — modelwijziging (identiteitssemantiek + audittype).
Gevolgen.
| Aspect | Gevolg |
|---|---|
| Inlogbaarheid | Verschil in hoofdletters of omringende spaties tussen registratie en login leidt niet meer tot een mislukte login — het meest gemelde testprobleem. |
| Migratie | De 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. |
| Support | De beheerder ziet in de audittrail wélke van de twee redenen een login deed mislukken, zonder dat de gebruiker die informatie krijgt. |
| Wachtwoordbeleid | Bewust 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:preloadErroren doen dan éénlocation.reload(). In combinatie met de bestaande PWA-autoUpdateverdwijnt de eenmalige crash-na-deploy. - First-party, cookieloze crash-rapportage: een gedeelde helper meldt onverwachte fouten (
window.onerror,unhandledrejectionen deErrorBoundary) aan een nieuw publiek endpointPOST /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.pathnamezonder querystring, user-agent) — nooit vrije gebruikerstekst, opnames of PII — alles defensief afgekapt, met een sessielimiet tegen rapportage-lussen. Het endpoint geeft altijd204(rapportage mag nooit zelf falen).
Status. Aanvaard (2026-07-13) — implementatie-/observability-beslissing, geen modelwijziging.
Gevolgen.
| Aspect | Gevolg |
|---|---|
| Deploy-UX | Een verouderde-chunk-fout na een deploy leidt tot een naadloze reload i.p.v. een foutpagina. |
| Waarneembaarheid | Client-side crashes komen mét stacktrace in Application Insights — voortaan te diagnosticeren i.p.v. te gissen. |
| Privacy | Consistent met ADR-036: cookieloos, geen third-party SDK, alleen metadata zonder PII (pad zonder querystring). |
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.
- Mailtype als sleutel. Elk sjabloon heeft een vaste
sleutel= het mailtype (bv.uitslag-klaar), pertaal. Zo hangt er een toekomstvaste structuur: een nieuw mailtype = een seed-rij + een placeholder-definitie, zónder de verzendcode te herschrijven. - 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. - 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
AcsUitslagNotificatiesopent per verzending een service-scope om het sjabloon uit de (scoped)AppDbContextte lezen. - 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.
| Aspect | Gevolg |
|---|---|
| Beheer zonder deploy | Onderwerp + body van het uitslag-seintje zijn aanpasbaar in het beheerportaal (tab E-mailteksten); geen code-wijziging of deploy meer nodig voor een tekstwijziging. |
| Toekomstvast | Latere mails (wachtwoordherstel e.d.) gebruiken hetzelfde mechanisme — nieuw mailtype = seed-rij + placeholder-definitie. |
| Robuustheid | Zonder beheerde rij of bij een DB-storing valt de app terug op de code-default; het seintje blijft werken. |
| Privacy/anonimiteit | Ongewijzigd 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.)
- Modelleren als synthetische sectie per categorie. De categorieën worden op de
Vraagbewaard als lijstOpenVraag({ sleutel, titel, subvragen }) met sub-itemsOpenSubvraag({ sleutel, tekst }). Elke categorie wordt tegelijk als een eigen sectieopen:<sleutel>(met de sub-vraag-sleutels als attributen) aan deBeoordelingsselectietoegevoegd. 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 uitopenVrageni.p.v. deKeuzelijst. Dit vermijdt een parallelle antwoord-opslag en dus extra migratie opOpinie. - Beheerbaar. De maxima zijn
maxOpenVragen(categorieën) enmaxOpenSubvragen(sub-vragen per categorie); de hulptekst voor de maker is beheerbare content (Keuzelijst-typeuitleg, sleutelopen-vragen) — alle zonder code-wijziging aanpasbaar, consistent met issue #20. - 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.
| Aspect | Gevolg |
|---|---|
| Minimale impact | Hergebruik van de bestaande Scores/sectie-machinerie; alleen een nieuw jsonb-veld op Vraag (+ migratie) en labelweergave. |
| Optioneel & simpel | Zonder open vragen verandert er niets; met maxOpenVragen = 0 is de functie volledig uit. |
| Beheerbaar | Aantal (instelling) en hulptekst (uitleg/open-vragen) zijn zonder deploy aanpasbaar. |
| Consistentie | Open 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.
- 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(slugprod) entesting(slugtest), beide inClermond-2. Dit vervangt het “aparte resourcegroep per omgeving”-voorstel: het label maakt omgevingen net zo onderscheidbaar, en eenpulumi destroyverwijdert uitsluitend de eigen, gelabelde resources. - Persistente resources in een apart Pulumi-project
infra/dns/(projectiseeyou-dns, stacksproductie/testing): per domein de Azure DNS-zone én het ACS-e-maildomein (EmailService + CustomerManagedDomain+SenderUsernamenoreply, met DKIM/SPF/verificatie-records in de zone). Een eigen project met eigen state is de enige vorm waarbij een omgevings-destroyde zone en de DKIM-sleutels aantoonbaar niet kán raken;protect: trueen een workflow zónder destroy-actie zijn de tweede vangrail. De nameservers worden éénmalig bij Vimexx gedelegeerd. - 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 óókApp__PortaalUrlenCors__Origins(afgeleid van het domein) via een explicieteWebAppApplicationSettings-resource — de omgeving is daarmee in éénupvolledig werkend, zonder handmatige na-configuratie. - Branch-per-omgeving.
maindeployt continu naar testing; promotie naar productie =mainmergen in de branchproductie, die naar de productie-omgeving deployt. Deci.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.
| Aspect | Gevolg |
|---|---|
| Up/down zonder DNS-schade | pulumi 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. |
| Kosten | Elke 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). |
| Reproduceerbaarheid | App-settings (incl. App__PortaalUrl, Cors__Origins, registry-credentials) worden bij elke up afgedwongen; handmatige setting-wijzigingen buiten Pulumi om overleven een up bewust niet. |
Mails 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 productie | Blijft in Clermond draaien (geen datamigratie); opruimen is een aparte, handmatige stap van de opdrachtgever. |
Zie Deployment — Meerdere omgevingen en infra/README.md.
Verschillen t.o.v. het prototype
Section titled “Verschillen t.o.v. het prototype”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:
| Onderwerp | Prototype (referentie) | Greenfield-doelarchitectuur | ADR |
|---|---|---|---|
| Backend-platform | Node / Express (JavaScript) | .NET / C# (typeveilig) | ADR-001 |
| Persistentie | JSON-bestanden + in-memory cache | PostgreSQL + EF Core (Npgsql) | ADR-002 |
| Structuur | Vlakke server.js | Gelaagd + DDD, modulaire monoliet per context | ADR-003 |
| Use-cases | Ad-hoc in routes | Dunne, expliciete handlers (geen MediatR) | ADR-004 |
| Datalevensduur | JSON-stores, ad-hoc opruimen | Vluchtig + persistent in één DB, retentie via BackgroundService | ADR-005 |
| Realtime | Niets / polling | SignalR voor push, REST voor mutaties; Redis-backplane bij schaal | ADR-006 |
| Wachtwoorden | Platte tekst | Argon2id (gebruikers) + ASP.NET Core Identity (beheer) | ADR-007 |
| Media-opslag | Lokaal filesystem | IMediaStore: lokaal/MinIO ↔ S3-compatibel, bytes in object-storage | ADR-008 |
| Foto vs. video | Video-only | Eén media-pipeline via MediaType { Video, Foto } | ADR-009 |
| Retentie | Uitschakelbare ENABLE_CLEANUP-flag | Retentie altijd aan (RetentieHostedService); verwijderen zodra de beoordeling klaar is, met instelbare veiligheids-timeout | ADR-010 |
| Ster-toekenning | In applicatielogica | Afdwingbaar via unieke index op Ster(vraag_id) | ADR-002 |
| Locatie | Vrij nationaliteit-veld + handmatige NL-regiolijst | Geen Land-veld (single-tenant per site, NL eerst); Regio als vlakke keuzelijst | ADR-037 |
| Aantallen & termijnen | Vaste getallen (10 opiniegevers, 30 min, …) | Via Beheer instelbaar (Systeeminstellingen) | ADR-017 |
| Te weinig kandidaten | Minder of geen opinies | Opnieuw toewijzen + AI-persona-vangnet | ADR-017 |
| Beoordeelde velden | Drie vaste secties | Beoordelingsselectie door de maker (standaard Gezicht/Kleding/Accessoires) | ADR-017 |
| Deelname-poort | n.v.t. | Credits als enige deelname-poort: beoordelen verdient, vragen kost; te weinig → eerst verdienen (vervangt reciprociteit en “eerst zelf beoordelen”) | ADR-017 |
| Credits & waardering | n.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 |
| Wachtwoordherstel | n.v.t. | Optioneel e-mailadres, uitsluitend voor herstel | ADR-007 |
| Uitslagnotificatie | n.v.t. | Optioneel per vraag (notificatieEmail): één mail met alleen een link via ACS Email, fail-graceful, adres na verzending gewist | ADR-023 |
| AI-persona-invulling | n.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 beheerder | ADR-024 |
| Vrije reactie | Altijd optioneel | Maker bepaalt met VrijeTekstGevraagd of er een vrije reactie gevraagd wordt (dan verplicht); alleen-vrije-tekst-vraag (zonder scores) mag | ADR-025 |
| AI-reactie & toelichting | n.v.t. | AI-persona geeft scores + een onderbouwde reactie en krijgt de toelichting mee (herkaderde, opt-in prompt) | ADR-025 |
| Aantal & herkomst & waardering | Vast (10 opiniegevers); ster-toekenning in code | Standaard 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 & uitbetaling | Loterij-ster + uitbetaling in applicatielogica | (Voorgesteld 2026-07-05) Verdiende ster via de consensus-ranking + mijlpalen; uitbetaling terug via PSP (KYC), mechaniek deels open | ADR-027 |
| Betaald advies | n.v.t. | (Voorgesteld 2026-07-05) Adviesniveaus 1–4/Expert met verplichte tekst en prijzen per beoordeling; geblurde teasers met ontgrendeling per beoordeling | ADR-028 |
| Geld & saldo | n.v.t. | (Voorgesteld 2026-07-05) Euro-saldo via iDeal/PSP met eenmalige welkomstbonus; strikt gescheiden van credits | ADR-029 |
| Onboarding | n.v.t. | (Voorgesteld 2026-07-05) Welkomsttekst, eerst-2-beoordelen-poort met cold-start-vangrail, tips-vensters; default 10 beoordelingen | ADR-030 |
| Frontend | Prototype-React in JavaScript | Greenfield React 19 + Vite + TypeScript (monorepo) | ADR-012 |
| Portalen / API | n.v.t. | Twee portalen op één API via autorisatiebeleid | ADR-013 |
| Hosting | n.v.t. | Cloud-agnostisch, Docker + scale-to-zero | ADR-015 |
Zie het Domeinmodel, de Termen en de overige Architectuur-pagina’s voor de uitwerking.