Kort samengevat. We draaien veel audits op apps gebouwd met Cursor, Claude Code, Bolt, Lovable en lange ChatGPT-sessies. De codebases verschillen enorm, maar de bevindingen vrijwel nooit. Elf problemen duiken steeds weer op, ruwweg op volgorde van hoe hard ze bijten: hardgecodeerde geheimen en inloggegevens, geen invoervalidatie (het injectieoppervlak), authenticatie die het vakje afvinkt maar niet het verzoek, nul testdekking, geen foutafhandeling op het ongelukkige pad, N+1-query's en prestaties aan het toeval overgelaten, verouderde afhankelijkheden met bekende CVE's die ongepatcht blijven, gedupliceerde variabelen en functies, geen consistente architectuur, complexe asynchrone state ingeklapt tot callbackchaos in plaats van stromen, en geen besef van de uitrolomgeving. Niets hiervan is exotisch. Het is allemaal voorspelbaar — en het is allemaal te herstellen zonder herbouw. Dit is de technische tegenhanger van onze gids voor oprichters over een AI-prototype opleveren; wil je het uit handen geven, dan is dat wat een audit van AI-code doet.
Wat een audit is — en niet is
Een audit is geen herbouw, en het is geen oordeel over of je de app wel met AI had moeten bouwen. Het is een gestructureerde lezing van een werkende codebase die één vraag beantwoordt: wat gebeurt er de eerste keer dat dit een echte aanvaller ontmoet, een echte verkeerspiek, of een echte wijziging over een half jaar? De bevindingen hieronder zijn geen hypothetische categorieën uit een checklist — elk daarvan is iets wat we in een echte opdracht hebben gevonden, geanonimiseerd en hersteld. We verwijzen doorlopend naar ons voorbeeldrapport en naar de dienst audit van AI-code, want daar gebeurt het herstel; dit stuk is het bewijs waarom het nodig is.
Waarom de bevindingen terugkeren
AI-programmeergereedschap optimaliseert op één ding: de kortste weg naar code die draait. Dat is werkelijk nuttig — je krijgt een idee in uren in werkende staat. Maar "draait in de demo" en "onderhoudbaar en veilig in productie" zijn verschillende doelen, en het gat ertussen is over projecten heen opmerkelijk consistent.
De reden is structureel. Een model dat code genereert heeft een smal contextvenster en geen herinnering aan de besluiten die het drie bestanden geleden nam. Het kan niet testen wat het net schreef, het heeft geen idee van je resterende middelen of je beveiligingshouding, en het heeft geen prikkel om de codebase op termijn samenhangend te houden. Dus maakt het elke keer de lokaal optimale keuze — en de som van lokaal optimale keuzes is een codebase die vandaag werkt en zich morgen tegen elke wijziging verzet.
Na genoeg audits vallen de fouten in dezelfde elf bakken. Hier zijn ze.
1. Hardgecodeerde geheimen en inloggegevens
Elke audit vindt dit, en het is precies het geurtje dat echt geld kost: .env-bestanden vastgelegd in de repository, API-sleutels en databasegegevens hard in de broncode, tokens van derden gebakken in clientbundels die naar elke browser gaan die de app laadt. Eén gelekte sleutel van OpenAI of Stripe kan binnen uren duizenden euro's aan ongeautoriseerde kosten opleveren — of een aanvaller je gegevensopslag zo in handen geven.
Waarom AI dit doet. Een sleutel hardcoderen werkt meteen; een geheimenbeheerder of omgevingsinjectie aansluiten niet, en het model heeft geen reden de tragere weg te verkiezen wanneer de snellere ook "draait". De kortste weg naar een werkende functie is vrijwel nooit de veilige.
Hoe we het herstellen. Geheimen gaan uit de broncode en in deugdelijk omgevingsbeheer. Alles wat ooit in git is vastgelegd behandelen we als reeds gecompromitteerd en wisselen we, we verwijderen het niet alleen — een git rm zonder wisselen laat de oude sleutel geldig in elke kloon en elke commitgeschiedenis. Dit is het niet-onderhandelbare deel van elke audit: een gelekte sleutel is een noodgeval, en die herstellen we eerst.
2. Geen invoervalidatie — het injectieoppervlak
SQL-injectie, XSS en prompt injection komen wijdverbreid voor in met AI gebouwde code, en de oorzaak is elke keer dezelfde: gebruikersinvoer gaat rechtstreeks een query, een sjabloon of een LLM-prompt in zonder validatie of ontsmetting ertussen. Eén kwetsbaar endpoint kan je hele database compromitteren of een aanvaller laten sturen wat je eigen AI-functies doen.
Waarom AI dit doet. Validatie is een tweede verzoek dat het model nooit kreeg. Het genereert de code die aan de gelukkige-padprompt voldoet — "neem het bericht van de gebruiker en sla het op" — en het gelukkige pad noemt nooit wat er geweigerd moet worden.
Hoe we het herstellen. Elke grens waar niet-vertrouwde invoer het systeem binnenkomt krijgt expliciete validatie en geparametriseerde query's of sjablonen, geen tekenreeksen aan elkaar plakken. Voor AI-functies in het bijzonder betekent dat de promptopbouw zelf als injectiegevoelige grens behandelen, niet alleen de databaselaag.
3. Authenticatie die het vakje afvinkt, niet het verzoek
Met AI gebouwde apps bouwen authenticatie vaak alleen aan de oppervlakte — er is een inlogscherm, en het werkt — maar de backend controleert de rechten nooit per verzoek. API-endpoints accepteren alles wat binnenkomt. Beheerroutes zijn bereikbaar zonder rolcontrole. Gebruiker A ziet de gegevens van gebruiker B door een ID in de URL te wijzigen — een onveilige directe objectverwijzing die in productie staat.
Waarom AI dit doet. "Voeg een inlogpagina toe" en "controleer of dit specifieke verzoek dit specifieke record mag aanraken" zijn verschillende problemen, en het model lost het op waarnaar het gevraagd werd. Authenticatie is zichtbaar in een demo; gaten in de autorisatie zijn onzichtbaar tot iemand ze uitbuit.
Hoe we het herstellen. We leggen elk endpoint naast wie het werkelijk mag aanroepen, niet naast wie het inlogscherm suggereert, en voegen de ontbrekende autorisatiecontroles per verzoek toe — eigenaarschapscontroles op records, rolcontroles op beheerroutes, en verzoeklimieten op alles waar een script op kan hameren.
4. Geen tests — elke uitrol is een gok
De meest voorkomende bevinding: er zijn helemaal geen tests. Geen dunne suite, geen wispelturige tests — nul. De app is gevalideerd door erdoorheen te klikken, en dat is het hele vangnet.
Dat is onzichtbaar tot precies het moment waarop het rampzalig is. Zonder tests is er geen manier om te weten of een wijziging iets brak, behalve hem uitbrengen en wachten tot een gebruiker klaagt. Elke uitrol wordt een handmatige regressieronde die niemand werkelijk uitvoert, dus herstructureren wordt eng, updates van afhankelijkheden worden overgeslagen, en de codebase verkalkt — niet omdat de code slecht is, maar omdat niemand hem durft aan te raken.
Waarom AI dit doet. Een functie genereren en tests voor die functie genereren zijn twee aparte verzoeken, en niemand deed het tweede. Het model schrijft graag tests als je erom vraagt, maar aan zichzelf overgelaten levert het het gelukkige pad op en stopt.
Hoe we het herstellen. We mikken niet op 100% dekking op dag één. We voegen een dunne laag toe waar die zich het best terugbetaalt: een rooktest dat de app start, tests rond de geld- en authenticatielogica, en een regressietest voor elke bug die we tijdens de audit oplossen. Dat alleen al maakt van uitrollen een routine in plaats van een gok.
5. Geen foutafhandeling op het ongelukkige pad
De app werkt precies zoals gedemonstreerd — zolang het netwerk nooit wegvalt, de externe API nooit een time-out geeft, en de gebruiker nooit iets onverwachts doet. Zodra een van die dingen gebeurt zijn de symptomen lelijk: een niet-afgevangen promise-afwijzing laat het hele verzoek crashen, een mislukte API-aanroep laat de interface eeuwig op een spinner staan, een exceptie toont de gebruiker een ruwe stacktrace in plaats van een bericht dat hem iets zegt.
Waarom AI dit doet. Het gelukkige pad is wat de prompt beschreef en wat de demo doorliep. Foutafhandeling is verdedigende code voor situaties die het model nooit is gevraagd zich voor te stellen, en ze voegt regels toe zonder de demo indrukwekkender te maken — dus is het het eerste wat onder een impliciet tijdsbudget sneuvelt.
Hoe we het herstellen. We lopen elke externe aanroep langs — API, database, bestandssysteem — en voegen de faaltak toe: opnieuw proberen met uitstel waar dat helpt, een terugval of een helder foutscherm waar dat niet zo is, en logging die vertelt wat er werkelijk gebeurde in plaats van een generiek "er ging iets mis". Het doel is dat een storing bij een derde je app netjes laat degraderen in plaats van hem onderuit te halen.
6. N+1-query's en prestaties aan het toeval overgelaten
Het lijstscherm dat in ontwikkeling met tien rijen direct laadt, komt in productie met tienduizend tot stilstand. De klassieke oorzaak is een N+1-query: één query om een lijst op te halen, en daarna per rij een aparte query voor de bijbehorende gegevens, zodat een scherm dat één rondgang naar de database zou moeten kosten er honderden kost. Ontbrekende indexen, ongebonden resultaatsets zonder paginering, en hele objecten laden terwijl er maar een veld of twee wordt getoond, zijn de gebruikelijke metgezellen. Onze gids over databases en indexen behandelt de mechaniek van waarom dit traag is en hoe een gezond queryplan eruitziet.
Waarom AI dit doet. Het N+1-patroon is de meest voor de hand liggende manier om de lus te schrijven, en het levert correcte uitvoer op — het model heeft geen terugkoppeling die zegt dat het aantal query's telt tot iemand het onder echte datavolumes meet, en dat doet een demo met een handvol rijen nooit.
Hoe we het herstellen. We profileren de werkelijke querypatronen onder realistische datavolumes, klappen de N+1-ketens in tot joins of gebundelde ophaalacties, voegen de ontbrekende indexen toe, en zetten paginering of limieten op alles wat een ongebonden set teruggeeft. Dit is doorgaans de prestatiewinst met de grootste hefboom in een audit, want één slecht lijstscherm kan het grootste deel van de laadtijd van een pagina verklaren.
7. Afdrijvende afhankelijkheden en CVE's
npm audit of het equivalent levert een muur bekende kwetsbaarheden op zodra iemand het draait — want niemand had dat gedaan. Pakketten staan vast op de versie die actueel was toen de AI-tool het project opzette, transitieve afhankelijkheden die niemand rechtstreeks koos dragen eigen CVE's mee, en er is geen proces om erachter te komen wanneer er een patch verschijnt.
Waarom AI dit doet. Het model kiest een pakket dat het directe probleem oplost en gaat verder; het heeft geen doorlopende band met je project die het later tot heroverwegen aanzet. Hygiëne rond afhankelijkheden is onderhoud, en niets aan een functie genereren zet onderhoud in gang.
Hoe we het herstellen. We draaien een scan op kwetsbare afhankelijkheden, patchen of vervangen alles met een bekende uitbuiting, en zetten een proces op — desnoods een eenvoudige geplande scan — zodat dit niet stilletjes opnieuw afdrijft zodra de audit eindigt.
8. Gedupliceerde variabelen en functies
Open een met AI gebouwde codebase en zoek naar dezelfde hulpfunctie voor datumopmaak. Je vindt hem vaak drie of vier keer — elke keer net iets anders, omdat elk apart is gegenereerd voor het scherm dat hem nodig had. Hetzelfde geldt voor validatieregels, API-clients, geldberekeningen en configuratieconstanten.
Duplicatie is niet alleen lelijk; het is een tijdbom voor correctheid. Moet de logica veranderen — een nieuwe belastingregel, een opgeloste afrondfout, een gewijzigd endpoint — dan moet je elke kopie vinden. Je mist er een. Nu zijn twee delen van de app het oneens over iets waarover ze het eens zouden moeten zijn, en dat meningsverschil is het volgende productie-incident.
Waarom AI dit doet. Het model doorzoekt de bestaande codebase zelden op een hulpfunctie die het kon hergebruiken. Het is vanuit zijn perspectief goedkoper de functie ter plekke opnieuw te genereren dan de bestaande te ontdekken en te importeren. Elke generatie is op zichzelf redelijk; het totaal is afdrijving.
Hoe we het herstellen. We zoeken de clusters bijna identieke code op, trekken één bron van waarheid eruit, en leiden elke aanroepplek daarlangs. Dit is een van de opruimacties met de grootste hefboom in de meeste audits: het krimpt de codebase en haalt hele categorieën "hier opgelost maar daar niet"-bugs weg.
9. Geen consistente architectuur
Deze is schokkend om voor het eerst te zien. Twee schermen in hetzelfde project zijn geschreven alsof twee verschillende teams eraan werkten: het ene haalt data op in de component, het andere via een servicelaag; het ene houdt state op de ene manier vast, het volgende doet het compleet anders; naamgeving, mapindeling en foutafhandeling wisselen per functie. Er is geen ruggengraat.
Een codebase zonder consistente architectuur is er een waarin elk bestand dat je opent een verrassing is. Een developer inwerken kost weken omdat er geen patroon te leren valt — alleen honderd bijzondere gevallen om te onthouden. Erger nog: waar patronen botsen, zitten precies de naden waar bugs broeden.
Waarom AI dit doet. Het model heeft geen blijvend beeld van "hoe deze app is gebouwd". Elke prompt is een verse start, dus grijpt het naar het patroon dat bij dat ene verzoek past. Over de levensduur van een project levert dat een lappendeken op — elk stuk op zichzelf verstandig, het geheel onsamenhangend.
Hoe we het herstellen. We kiezen één architectuur die bij het project past — geen dogmatische, een passende — en laten de codebase daar stapsgewijs naartoe convergeren, zodat een developer die één functie leert kan voorspellen hoe de volgende werkt.
10. Stroomgebaseerde state vermeden — recht de callbackchaos in
Dit is de technisch interessantste fout, en degene die stilletjes de lastigste functies breekt. Door AI gegenereerde code neigt ertoe stroom- en reactieve statemodellen te vermijden ten gunste van imperatieve callbacks. In plaats van "deze waarde verandert in de tijd en de interface reageert" te modelleren, sluit het een callback aan, die een andere callback aanroept, die een vlag zet, die een derde afvuurt — en het resultaat is callbackchaos.
Bij eenvoudige schermen merk je het nauwelijks. Maar zodra de state werkelijk complex is — een formulier in meerdere stappen met validatie over velden heen, een dashboard dat live bijwerkt, alles met vertragen, opnieuw proberen, annuleren of optimistisch bijwerken — valt de callbackaanpak uit elkaar. Het klassieke symptoom is het formulier dat bijna werkt: het valideert, maar de fout verdwijnt op het verkeerde moment; het verstuurt, maar een dubbele tik vuurt hem twee keer af.
Waarom AI dit doet. Imperatieve callbacks zijn het meest voorkomende patroon in de trainingsdata en het makkelijkst stuk voor stuk te genereren. Reactieve en stroomgebaseerde modellen vragen dat je de hele toestandsmachine tegelijk in gedachten houdt — precies waar een generator met beperkte context het slechtst in is.
Hoe we het herstellen. We wijzen de functies met complexe state aan en herbouwen hun statelaag deugdelijk — als stromen of als een reactief statemodel dat bij de stack past — zodat de interface een functie van de state is in plaats van een stapel callbacks die met elkaar racen.
11. Geen besef van de uitrolomgeving
Het model schrijft code alsof die als één proces op één machine draait — want van binnenuit de prompt is dat de enige omgeving die het kan zien. Het heeft geen idee hoeveel instanties er zullen draaien, welke beheerde diensten er al zijn, of hoe verkeer wordt gerouteerd. Dus grijpt het naar de eenvoudigst mogelijke topologie, en die standaardkeuze breekt stilletjes zodra de app werkelijk wordt uitgerold.
De symptomen zijn altijd dezelfde. State die in het geheugen van het proces leeft — een cache, sessies, tellers voor verzoeklimieten — werkt perfect op één instantie en loopt stilletjes uiteen zodra er een tweede replica achter de loadbalancer opkomt. Achtergrondtaken vuren op elke instantie in plaats van één keer, zodat de e-mail drie keer uitgaat.
Waarom AI dit doet. Het heeft geen beeld van je infrastructuur. Het weet niet dat je al Redis, een berichtenwachtrij en objectopslag hebt — dus bouwt het ze in het geheugen opnieuw. De uitroltopologie is precies de context die een prompt niet kan bevatten.
Hoe we het herstellen. We brengen de werkelijke uitrol in kaart en verplaatsen gedeelde state naar waar ze hoort: cache, sessies en locks naar Redis of de database, bestanden naar objectopslag, terugkerend werk naar een echte planner of wachtrij. Het resultaat is code die horizontaal schaalt.
Hoe we ze vinden
Elke bevinding hierboven begint met een geautomatiseerde ronde en eindigt met een mens die de code leest. De automatisering — statische analyse, scannen op afhankelijkheden en geheimen, profileren van API-kosten — is wat een AI-versnelde audit snel maakt: ze ruimt de categorieën op die mechanisch te vinden zijn (bevinding 1, 4, 6 en 7 hierboven duiken hier vrijwel meteen op), zodat de tijd van onze engineers zich concentreert op de categorieën die oordeelsvermogen vragen — autorisatielogica, architectuur, en of een bepaald foutpad er voor jouw product werkelijk toe doet. Geen van beide helften werkt alleen: automatisering alleen mist alles wat begrip vraagt van waar de code voor is, en handmatig beoordelen alleen schaalt niet naar een echte codebase in een week. De volledige uitsplitsing van het proces staat op de dienstpagina audit van AI-code.
Wat er in het rapport staat
Bevindingen komen niet als ruwe lijst. Elke is op ernst gerangschikt — kritiek, hoog, gemiddeld, laag — met een uitleg van het risico in gewone taal, een demonstratie waar dat past, en een concrete oplossing, precies de vorm die elke bevinding hierboven volgde. Wil je de opzet zien voordat je je ergens aan verbindt, vraag dan een geanonimiseerd voorbeeldrapport aan — dezelfde rapportstructuur die een echte opdracht oplevert, met identificerende gegevens van de klant verwijderd.
Het patroon achter het patroon
Doe een stap terug en de elf bevindingen delen één oorzaak: AI optimaliseert elke generatie lokaal, en niemand optimaliseert de codebase als geheel. Geheimenbeheer, invoervalidatie, autorisatie, tests, foutafhandeling, queryprestaties, hygiëne rond afhankelijkheden, ontdubbeling, architectuur, statemodellering en besef van de uitrolomgeving zijn allemaal eigenschappen van het hele systeem. Ze kunnen niet prompt voor prompt ontstaan, want geen enkele prompt kan het geheel zien. Precies dat gat dicht een menselijke beoordeling.
Het geruststellende deel is dat niets hiervan betekent dat het met AI gebouwde fundament verspild is. De functies werken; het product is echt. Wat ontbreekt is het bindweefsel — en dat toevoegen gaat veel sneller dan vanaf nul herbouwen.
Veelgestelde vragen
Krijg de bevindingen voor jouw codebase
Heb je een met AI gebouwde app en herken je er een paar van deze elf, dan loop je niet achter — je zit precies waar vrijwel elke door AI gegenereerde codebase belandt. De oplossing is geen herbouw; het is een gerichte audit die het bindweefsel toevoegt dat de AI niet kon.
Haal je codebase door een audit van AI-code, vraag een voorbeeldrapport aan om eerst de opzet te zien, of plan een gratis analyse en we vertellen je welke van de elf je grootste risico is, wat het kost om die te herstellen, en geven je een offerte met vaste scope — geen gok.

