Blog
Een avond vibe coden en wat ik daarvan leerde
juli 7, 2026
Blog
juli 7, 2026

Een paar weken geleden zat ik met mijn Ability-collega Jan Kooistra aan de koffie. Ability draait sinds kort weer hard met nieuwe klanten, terwijl dat de afgelopen maanden wel anders was. Ik vroeg Jan wat hij anders deed. Zijn antwoord was eenvoudig: hij werkte zijn netwerk intensief af, koffie hier, diner daar, veel online contact, en hij postte elke dag iets relevants op LinkedIn. Niet om viral te gaan, maar om top of mind te blijven, het gesprek op gang te houden en af en toe een lead binnen te halen.
Ik ben commercieel manager in de IT, dus ik voel dagelijks wat AI met onze sector en met Smartshore doet. Ik volg het nieuws, praat met collega's, en heb als niet-techneut een redelijk beeld van wat AI kan en hoe snel het gaat. Smartshore maakt daar natuurlijk onderdeel van uit. We gebruiken AI in al onze projecten, hebben onze eigen tool Ada Lovelace en onze developers schrijven met AI de best mogelijke code.
Om beter te begrijpen wat ik als commercieel persoon eigenlijk verkoop, besloot ik het zelf te ervaren. Ik ging vibe coden, een applicatie bouwen die voor mij LinkedIn-posts genereert, plant en publiceert, zodat ik nooit meer zou vergeten om dagelijks te posten. Iedereen bij Smartshore werkt met Claude; ik ook, maar tot een paar dagen eerder had ik alleen uit nieuwsgierigheid naar Claude Code gekeken. Deze keer ging ik er echt mee aan de slag.
Met hulp van Claude.ai stelde ik mijn briefing voor Claude Code op. Achteraf gezien laat die briefing precies zien hoe het zit met mijn technische kennis: met de juiste hulp kon ik een prompt produceren die klinkt als die van een ervaren ontwikkelaar, terwijl ik van een flink deel van de termen erin de exacte betekenis niet kende.
Bouw een complete webapplicatie waarmee ik LinkedIn-posts kan laten genereren, goedkeuren en automatisch publiceren op mijn persoonlijke LinkedIn-account. Ik ben de enige gebruiker van deze applicatie, dus houd de architectuur eenvoudig: geen multi-user-functionaliteit, geen registratiesysteem, alleen een simpele lokale inlogbeveiliging zodat niemand anders op mijn netwerk de app kan bedienen.
Werkwijze: presenteer eerst een kort implementatieplan met de gekozen technologie, de projectstructuur en de volgorde van bouwen. Wacht op mijn akkoord voordat je begint met de daadwerkelijke implementatie. Raadpleeg voor de integratie met de Anthropic API de actuele documentatie op https://docs.claude.com en gebruik het daar aanbevolen actuele model, geen modelnaam uit je eigen geheugen.
De applicatie moet aan de volgende tien eisen voldoen.
Eén. Doel en kernflow. De app genereert conceptposts met AI, legt die aan mij voor ter goedkeuring, en publiceert goedgekeurde posts op een door mij gekozen moment op mijn persoonlijke LinkedIn-account. Niets verschijnt ooit op LinkedIn zonder mijn expliciete goedkeuring per individuele post.
Twee. LinkedIn-integratie via de officiële API. Gebruik OAuth 2.0 met de scope w_member_social en publiceer via het actuele versioned Posts-endpoint van LinkedIn. Bouw de OAuth-flow zo dat ik eenmalig inlog via LinkedIn en de app het access token veilig opslaat. Houd er rekening mee dat LinkedIn-tokens na zestig dagen verlopen: toon ruim van tevoren een waarschuwing in de interface en bied een knop om opnieuw te autoriseren. Gebruik uitsluitend de officiële API, geen scraping of browserautomatisering. Documenteer in een aparte SETUP.md welke stappen ik zelf moet doorlopen in het LinkedIn Developer Portal, inclusief het activeren van het product Share on LinkedIn en het instellen van de redirect-URL.
Drie. AI-contentgeneratie met een stijlprofiel. Genereer posts via de Anthropic API. De systeeminstructie voor het model staat in een apart, door mij bewerkbaar configuratiebestand met daarin het stijlprofiel: posts in het Nederlands, geschreven in een persoonlijke en directe toon vanuit de ik-vorm, zakelijk maar niet corporate, zonder emoji, zonder hashtagrijen, met normale Nederlandse leestekens en zonder opsommingstekens of opmaaksymbolen. In hetzelfde bestand staat een contextblok over mijn werk en onderwerpen, dat ik zelf kan vullen en aanpassen. In de interface kan ik per generatie een onderwerp of korte briefing meegeven, of de app een onderwerp laten voorstellen op basis van het contextblok.
Vier. Goedkeuringsworkflow met bijsturing. Elke gegenereerde post komt in een wachtrij met de status ter goedkeuring. Per post heb ik vier opties: goedkeuren, afkeuren, de tekst handmatig bewerken in een editor, of een herschrijfinstructie meegeven in vrije tekst, bijvoorbeeld maak het korter en scherper of haal de verkooptoon eraf. Bij een herschrijfinstructie stuurt de app de bestaande post plus mijn instructie naar de AI en toont het resultaat als nieuwe versie, met de mogelijkheid om eerdere versies terug te zien en terug te zetten.
Vijf. Planning en publicatie. Goedgekeurde posts kan ik inplannen op een specifieke datum en tijd in de tijdzone Europe/Amsterdam. Een achtergrondproces publiceert de post op het ingestelde moment. De interface toont een kalender- of lijstoverzicht van alle geplande posts. Na publicatie krijgt de post de status geplaatst, inclusief tijdstip en de URL van de post op LinkedIn als de API die teruggeeft. Bouw een waterdichte beveiliging tegen dubbele publicatie, ook als het achtergrondproces herstart.
Zes. Veilig sleutelbeheer. Alle gevoelige gegevens, waaronder de LinkedIn client secret, het access token en de Anthropic API-key, staan uitsluitend in omgevingsvariabelen of een lokaal .env-bestand. Lever een .env.example met placeholders en een .gitignore die het echte .env-bestand en de database uitsluit van versiebeheer. Geen enkele sleutel mag in de broncode of in logbestanden terechtkomen.
Zeven. Opslag en statusbeheer. Gebruik een lokale SQLite-database. Elke post doorloopt een duidelijke statusmachine: concept, ter goedkeuring, goedgekeurd, gepland, geplaatst, afgekeurd. Bewaar per post alle versies van de tekst, het onderwerp of de briefing, de gebruikte herschrijfinstructies en alle relevante tijdstippen. Afgekeurde en geplaatste posts blijven doorzoekbaar in een archief.
Acht. Foutafhandeling en limieten. Vang mislukte publicaties netjes af met maximaal drie automatische herpogingen met oplopende wachttijd. Respecteer de rate limits van zowel de LinkedIn API als de Anthropic API. Als publicatie definitief mislukt, krijgt de post de status mislukt en toont de interface prominent wat er misging en wat ik kan doen. De app mag nooit stilletjes falen.
Negen. Logging en audittrail. Houd een logboek bij van elke betekenisvolle actie: generatie, herschrijving, handmatige bewerking, goedkeuring, afkeuring, inplanning, publicatiepoging en resultaat, telkens met tijdstip en de definitieve tekst. Dit logboek is inzichtelijk via de interface. Log nooit API-sleutels of tokens.
Tien. Testmodus. Bouw een dry-run-stand, in te schakelen via een instelling, waarin de volledige flow werkt maar de publicatiestap wordt gesimuleerd: de app logt wat er gepubliceerd zou worden zonder daadwerkelijk iets naar LinkedIn te sturen. De interface maakt op elk scherm duidelijk zichtbaar of de testmodus actief is. Lever daarnaast geautomatiseerde tests voor de statusmachine, de beveiliging tegen dubbele publicatie en de foutafhandeling.
Lever ten slotte een README op met installatie-instructies, een uitleg van de mappenstructuur en een beschrijving van hoe ik het stijlprofiel en het contextblok aanpas.
Technologiekeuze
Backend: Node.js + TypeScript, Express. Eén proces dat zowel de API als de achtergrondscheduler (via node-cron) draait — past bij "simpel, single-user".
Frontend: React + Vite + TypeScript, simpele SPA (geen complexe state-library nodig, React Query voor data-fetching).
Database: SQLite via better-sqlite3 (synchroon, simpel, geen aparte DB-server).
Auth: simpele sessie-cookie met één wachtwoord (uit .env, gehasht vergeleken), geen user-tabel.
AI-model: claude-sonnet-4-6 (beste balans snelheid/kwaliteit/kosten voor tekstgeneratie), als env-variabele instelbaar.
LinkedIn: OAuth 2.0 Authorization Code flow, scope w_member_social, publiceren via POST /rest/posts (versioned API, LinkedIn-Version header).
Projectstructuur
linkedin-poster/
├── server/
│ ├── src/
│ │ ├── index.ts (Express app + scheduler bootstrap)
│ │ ├── db/ (schema, migraties, sqlite connectie)
│ │ ├── routes/ (auth, posts, linkedin-oauth, settings, logs)
│ │ ├── services/ (anthropic.ts, linkedin.ts, scheduler.ts, publisher.ts)
│ │ ├── middleware/ (auth-check)
│ │ └── config/ (style-profile.json - bewerkbaar stijlprofiel + context)
│ └── data/ (sqlite db, .gitignored)
├── client/ (React app: dashboard, post-editor, kalender, archief, logs, instellingen)
├── .env.example
├── .gitignore
├── SETUP.md
└── README.mdIk had die tien punten niet zelf uit mijn hoofd kunnen opschrijven. Maar met Claude.ai naast me kon ik het wel formuleren, en dat is precies waar het later zou gaan wringen: ik kon een technisch klinkende briefing produceren zonder de onderliggende techniek echt te begrijpen.
Met mijn beperkte technische kennis leek het me een prima aanpak. Ik zei ja en de bouw begon.
Wat volgde was een avond vol vragen waarvan ik het bestaan niet kende: een sessiegeheim, een wachtwoord, API-sleutels, een client ID. Ik installeerde Node, Git en later Docker, en leerde werken met een terminal, op een gegeven moment zelfs met drie tegelijk open, één om commando's uit te voeren, één voor de frontend en één voor de backend. Het ging niet meteen goed. Fouten kwamen voorbij, ik gaf ze terug aan Claude Code, en stap voor stap kwamen we verder. Terwijl collega's om vijf uur naar de borrel gingen, bouwde ik door, tot mijn tokens opraakten en ik moest wachten. Ik sloot alsnog aan bij de borrel, en toen iedereen naar huis ging, bouwde ik later die avond verder, tot ik om acht uur haastig naar huis fietste om mijn dochters naar bed te brengen. Terwijl zij hun tanden poetsten, maakte ik de applicatie af.
De LinkedIn Poster werkte, had alle functies die ik op dat moment wilde, en ik was apetrots. In één avond, met weinig meer dan een goede briefing en een hoop doorzettingsvermogen, had ik iets gebouwd dat ik eerder nooit voor mogelijk had gehouden.
De volgende ochtend appte ik enthousiast met Olaf Molenveld (Technology Advisor bij CircleCI), een zeer goede vriend die ooit zelf een IT-bedrijf had en verkocht, en nu bij CircleCI werkt. Olaf is het type dat altijd de vraag stelt die je liever niet hoort, ook privé.
Olaf begon onschuldig:
"Gebruik je gewoon de officiële LinkedIn API's?"
"OAuth, neem ik aan?"
"Welke stack is het, TypeScript?"
En toen kwamen de vragen waarvan ik wist dat ze zouden komen:
"Wat doet dit eigenlijk anders of beter dan gewoon zelf posten? "
"Wat doet deze app functioneel, hij plaatst toch dingen op LinkedIn die door een taalmodel zijn gegenereerd?"
"Waarom niet gewoon een bestaande tool daarvoor gebruiken?"
Ik beet eerst even terug:
"Ik ben een tech-noob die trots is dat hij een webapplicatie heeft gebouwd."
Olaf vergeleek het daarna met iemand die ‘painting by numbers’ doet en zich dan trots schilder noemt, maar hij had wel een punt.
"Er komt hosting aan, zei hij, er komen bugs, er komen API-wijzigingen. Hoe ziet de ROI over een jaar uit? Waar host je dit eigenlijk? "
Olaf zag steeds meer van dit soort projecten, gemaakt door één persoon met een taalmodel, nooit echt getest, die niemand gaat gebruiken zodra ze open source gaan. Voor eigen gebruik is het een prima optie, zei hij, hij had zelf al een paar van dit soort tools gebouwd. Het deed hem denken aan Visual Basic, MS Access en Excel-macro's van vroeger.
“Iedereen kan coden met Claude van Ravensburger” ;)
“Maar mijn vragen zijn oprecht interesse hoor.”
“Ff bellen? Ik heb nl wel een serieuze vraag”
Dat telefoontje was verhelderend voor ons beiden, want ik had al beseft dat de applicatie ergens zou moeten draaien en dat ons DevOps-team de code zou moeten reviewen. De eerste stap die daarvoor nodig was, was ervoor zorgen dat mijn code in een repository op ons Smartshore Bitbucket-account terechtkwam. En dat is waar het voor mij als tech-noob mis begon te gaan.
Om het onder ons eigen beheer te krijgen, kreeg ik enkele opdrachten mee: het moet in een container draaien en de code moet op Bitbucket komen. Mijn collega Jonelle Da Cunha werd erbij gehaald om ervoor te zorgen dat de container goed lokaal draaide, de data goed werd opgeslagen en alles netjes naar Bitbucket ging.
Vanaf hier volgt het verhaal van mijn collega Jonelle:
Toen ik erbij werd gehaald, was mijn eerste reactie verbazing. Remko had, met alleen theoretische kennis van development, een volledig werkende applicatie gebouwd die in zijn browser draaide. De opdracht leek simpel: zorg dat de Docker-container lokaal goed draait, dat data goed wordt opgeslagen, en dat alles naar Bitbucket gaat. Het opzetten van Bitbucket-toegang liep al vast op onbekendheid met SSH op Windows en een paar verkeerde aannames over de systeemconfiguratie. Met hulp van Claude kwamen we erdoorheen, al bleef één probleem onverklaarbaar: het systeem probeerde verbinding te maken met een andere remote repository die we niet hadden ingesteld. Pas veel later zou dat duidelijk worden.
Het grotere probleem kwam later. Remko vroeg op een dag waar al zijn posts gebleven waren. Geen ontwikkelaar wil horen dat klantdata is verdwenen, zeker niet als er geen wijzigingen waren die dat hadden moeten veroorzaken. Ik had specifiek een volume op de Docker-container ingesteld om data te bewaren. Onderzoek wees uit dat de data op de juiste plek stond, maar leeg was. Het bestandsformaat dat de applicatie gebruikte, was op een onbekend moment veranderd. Met twaalfduizend regels code over vijfentwintig commits, zonder duidelijke commitstructuur, was er geen manier om terug te spoelen naar een eerdere werkende versie.
Later bleek dat er via de Claude-desktopapplicatie wijzigingen waren gepusht naar GitHub, terwijl het project inmiddels naar Bitbucket was verhuisd. GitHub had mijn aanpassingen niet, dus wat was er precies gepusht? Er waren geen nieuwe commits of branches zichtbaar, maar er was duidelijk iets gepusht. Het bleek dat er wijzigingen waren gedaan binnen een eigen virtuele omgeving, van waaruit was gepusht zonder dat dit lokaal zichtbaar was geweest. Op dat moment installeerde ik voor Remko Visual Studio Code met Claude Code erin, zodat wijzigingen voortaan lokaal zichtbaar zouden zijn, we konden teruggaan naar een werkende staat als iets brak, en hij met één klik kon committen en pushen in plaats van git-commando's te onthouden.
En net toen ik dacht dat het achter me lag, kreeg ik een vraag van een DevOps-collega: hoe is de applicatie opgebouwd, hoe is de scheiding tussen frontend en API, en waarom is de container zo ingericht? Heel normale vragen, die iedere ontwikkelaar over zijn eigen applicatie zou moeten kunnen beantwoorden. Maar noch Remko, noch ik kon ze beantwoorden.
En dat is precies het punt van vibe coding. Het kan iemand zonder ontwikkelaarservaring in een avond een werkende applicatie geven. Maar werkend is niet hetzelfde als begrepen. En zodra er iets breekt, de applicatie naar productie moet, of een collega een volkomen redelijke vraag stelt over hoe het is gebouwd, wordt die kloof jouw probleem.
Bij Smartshore noemen we die stap van werkend prototype naar veilige productieomgeving Vibe Code To Production. Niet om het enthousiasme van zelf bouwen te ontmoedigen, ik zou het zelf morgen weer doen, maar om teams te helpen die stap veilig te zetten, zodat een goed idee niet vastloopt op verdwenen data, een onbegrepen codebase of vragen die niemand kan beantwoorden.