<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>All Subjects Matter</title>
    <link>https://www.allsubjectsmatter.nl/nl/</link>
    <description>Een dag uit het leven van een CEO/CTO van 333travel en een gepassioneerde UI/UX- en techliefhebber. En alles waar ik verder over schrijf.</description>
    <language>nl</language>
    <image><url>https://www.allsubjectsmatter.nl/images/icon.png</url><title>All Subjects Matter</title><link>https://www.allsubjectsmatter.nl/nl/</link></image>
    <lastBuildDate>Wed, 11 Mar 2026 09:54:10 +0100</lastBuildDate>
    <atom:link href="https://www.allsubjectsmatter.nl/nl/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Kan een AI-gesprek een verkoopkanaal voor reizen zijn? Deel 2: de technische architectuur</title>
      <link>https://www.allsubjectsmatter.nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/</link>
      <guid isPermaLink="false">69b12cfe8819c400011f388b</guid>
      <pubDate>Wed, 11 Mar 2026 09:54:10 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[AI and LM]]></category><category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[De technische deep-dive over hoe we Joy hebben gebouwd: een RAG-pipeline met vector search en reranking, een geheugensysteem dat vier iteraties kostte, en de UI/UX-keuzes die een antwoord van 25 seconden sneller laten voelen.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/the-technical-architecture-behind-a-conversational-travel-channel/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>In <a href="https://www.allsubjectsmatter.nl/nl/nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/" data-preview>Deel 1</a> schreef ik waarom we een AI-reisinterface vanaf nul bouwden, wat we leerden van onze eerste mislukte poging in 2019, en waarom een plugin je nooit een volledig geïntegreerde conversationele ervaring geeft. Dit deel gaat over het hoe. De architectuur, de zoekpipeline, het geheugenprobleem dat vier pogingen kostte om op te lossen, en het UI/UX-werk dat 25 seconden aanvoelt als 5.</p>
<h2 id="the-architecture">De architectuur</h2>
<p>Omdat dit een techblog is en geen marketingpagina, laat ik je zien wat er echt draait. Dit is geen weekendproject. Dit is productie-infrastructuur.</p>
<pre class="line-numbers language-text"><code class="language-text">Frontend (React &#43; TypeScript &#43; Zustand)
    │
    │  WebSocket (real-time, bidirectional)
    │
FastAPI Backend
    ├── Query Orchestrator
    │     ├── LLM Client (multi-provider with automatic fallback)
    │     ├── Tool Registry (9 specialized tools)
    │     └── Observer System (async post-turn analysis)
    ├── RAG Pipeline
    │     ├── Vector Database (3 content collections)
    │     ├── Embedding Engine
    │     └── Reranker (dual provider, switchable)
    ├── Conversation Memory (dual storage)
    │     ├── SQL (message history)
    │     └── Vector DB (semantic search over past messages)
    ├── Security Layer
    │     ├── Input Guard (prompt injection defense)
    │     ├── Output Guard (credential leak prevention)
    │     └── Rate Limiting &#43; IP Management
    ├── MCP Server (Model Context Protocol)
    │     └── External AI assistant access to our data
    └── Lead Management
          ├── Proposal Generation
          └── Specialist Handoff</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>De frontend is een React-applicatie met real-time WebSocket-communicatie. Geen polling. Geen request-response-cycli waarin je op een compleet antwoord wacht. Elke token streamt in real-time, gebufferd op 50ms-intervallen om flikkering in de UI te voorkomen. Productresultaten worden naar de interface gepusht zodra ze gevonden zijn, nog voordat de LLM aan zijn antwoord begint.</p>
<p>De backend is Python/FastAPI met een modulair toolsysteem. De LLM genereert niet alleen tekst. Hij heeft toegang tot 9 gespecialiseerde tools en beslist zelfstandig welke hij gebruikt. Reizen zoeken? Dan roept hij <code>search_products</code> aan. Informatie over een bestemming nodig? Dan roept hij <code>search_knowledge</code> aan. Terugroepen wat eerder besproken is? Dan doorzoekt hij het gespreksgeheugen semantisch.</p>
<blockquote>
<p>De LLM genereert geen antwoorden. Hij orkestreert tools en stelt een antwoord samen op basis van echte data.</p>
</blockquote>
<p>De LLM-laag draait met een multi-provider fallback-systeem. Valt de primaire provider uit, dan schakelt het systeem automatisch over naar de secundaire. Geen downtime. Geen foutmeldingen. De klant merkt er niets van.</p>
<p>Er draait al een MCP-server, het protocol dat ik beschreef in mijn Web 4.0-post. Externe AI-assistenten kunnen onze reisdata er al door bevragen. Het is nog vroeg, maar de infrastructuur voor AI-to-business-communicatie staat live. Als de AI-assistent van Lisa de producten van 333travel wil doorzoeken, bestaat het endpoint al.</p>
<h3 id="the-rag-pipeline-where-the-real-work-happens">De RAG-pipeline: waar het echte werk gebeurt</h3>
<p>RAG, Retrieval-Augmented Generation, is de ruggengraat van het hele systeem. Het is wat &ldquo;GPT met een kennisbank&rdquo; scheidt van &ldquo;een AI die je product echt kent.&rdquo; Maar slecht uitgevoerde RAG is bijna erger dan geen RAG.</p>
<p>We onderhouden drie aparte vectorcollections:</p>
<table>
	<thead>
			<tr>
					<th>Collection</th>
					<th>Wat erin zit</th>
					<th>Wat het doet</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Travel Products</td>
					<td>Roundtrips, hotels, tours, cruises</td>
					<td>Drijft de productzoekopdracht aan</td>
			</tr>
			<tr>
					<td>Travel Details</td>
					<td>Dagprogramma&rsquo;s, excursies, inbegrepen services</td>
					<td>Beantwoordt specifieke vragen over reisschema&rsquo;s</td>
			</tr>
			<tr>
					<td>Travel Knowledge</td>
					<td>Blogs, bestemmingengidsen, reisinformatie</td>
					<td>Levert context en inspiratie</td>
			</tr>
	</tbody>
</table>
<p>Stelt een klant een vraag, dan wordt de query ge-embed en tegen de relevante collection gegooid. Maar hier komt het cruciale deel: <strong>we pakken niet gewoon de beste vector-matches en noemen dat een resultaat.</strong></p>
<p>Vector similarity alleen is middelmatig. Het brengt je in de juiste richting, maar levert niet de beste resultaten. Daarom doen we een brede fetch, aanzienlijk meer resultaten dan nodig, en halen die door een reranker. De reranker beoordeelt elk query-documentpaar op werkelijke semantische relevantie, niet alleen op embedding-nabijheid.</p>
<p>Het verschil is enorm. Het is het verschil tussen &ldquo;hier zijn reizen die een paar van je woorden bevatten&rdquo; en &ldquo;hier zijn reizen die daadwerkelijk matchen met wat je zoekt.&rdquo;</p>
<p>Maar het gaat verder. Nog voordat de query de vectordatabase raakt, gaat hij door een fuzzy normalisatielaag. Elke land- en plaatsnaam in ons systeem wordt bij het opstarten gecachet. Typt een klant &ldquo;Bali&rdquo;, dan weten we dat Indonesië bedoeld wordt. Schrijft iemand &ldquo;Tailand&rdquo;, dan weten we wat er bedoeld wordt. Aliassen, spellingvarianten, taalverschillen: allemaal opgelost voordat de zoekopdracht begint. Zonder dit krijg je lege resultaten op queries die wel hadden moeten matchen. En daarbovenop combineert de zoekopdracht semantische similarity met metadatafilters. Land, duur, producttype: dat zijn geen velden in een database. Het zijn actieve filters die samen met de vectorsearch de resultaten verfijnen, nog voordat de reranker begint.</p>
<p>Na het reranken splitsen de resultaten zich in twee paden. De volledige set gererankte resultaten gaat meteen naar de frontend, maar geblurd. De klant ziet productkaarten verschijnen terwijl Joy nog aan het nadenken is. Het toont voortgang zonder te overweldigen. Ondertussen krijgt de LLM een uitgedunde versie van die resultaten, alleen de velden die hij nodig heeft om te redeneren. Deze projectie bespaart ruwweg een derde van de tokens, wat snellere antwoorden en lagere kosten betekent. Daarna stelt Joy haar antwoord samen en bespreekt ze de 4 of 5 reizen die echt passen bij de vraag. Alleen die besproken producten worden zichtbaar voor de klant. De rest verdwijnt. Wat de klant ziet is een gecureerde, doordachte selectie. Geen lijst met 10 zoekresultaten. Een aanbeveling van 5 die echt logisch zijn.</p>
<blockquote>
<p>Brede fetch → Rerank → Project. Drie stappen die het verschil maken tussen een zoekresultaat en een aanbeveling.</p>
</blockquote>
<h3 id="the-memory-problem">Het geheugenprobleem</h3>
<p>Dit is het onderdeel dat de meeste iteraties kostte om goed te krijgen.</p>
<p>Conversationele AI heeft een smerig geheimpje: het onthoudt niets. Elke LLM-call is stateless. Het model heeft geen idee wat er eerder gezegd is, tenzij je de context expliciet aanlevert. En er is een grens aan hoeveel context je kunt aanleveren.</p>
<p>Voor een simpele Q&amp;A-chatbot maakt dat niet uit. Iemand stelt een vraag, krijgt een antwoord, klaar.</p>
<p>Maar voor een reisassistent? Geheugen is alles.</p>
<p>Een klant zegt: &ldquo;Ik wil twee weken naar Thailand.&rdquo; Drie beurten later: &ldquo;Maak er eigenlijk drie weken van. En ik wil Laos erbij.&rdquo; Vijf beurten later: &ldquo;Wat was die reis die je eerder liet zien, met de kookles?&rdquo;</p>
<p>Zonder geheugen is elke beurt een leeg blad. Het systeem weet niets van Thailand. Niets van de wijziging naar drie weken. Niets van Laos. En al helemaal niets van de reis met de kookles van zeven berichten geleden.</p>
<blockquote>
<p>Zonder geheugen is elk bericht een eerste date. Je AI heeft geen idee wat er eerder gebeurd is.</p>
</blockquote>
<p>Dit losten we niet in één poging op. Niet in twee. Zelfs niet in drie.</p>
<p>De eerste versie was simpel: de laatste 10 vraag-antwoordparen in de LLM-context stoppen. Dat werkte voor korte gesprekken. Maar onze gesprekken zijn niet kort. Klanten verkennen, vergelijken, komen terug op eerdere ideeën, bedenken zich. Tien beurten is niet genoeg als iemand twintig minuten over drie verschillende landen heeft gechat.</p>
<p>Dus probeerden we op de achtergrond samenvattingen te genereren. Het idee was goed: het gesprek samenvatten tot kernpunten, zodat de LLM altijd het volledige beeld heeft zonder de hele context window vol te gooien. De praktijk was anders. De LLM begon details te hallucineren in de samenvattingen. Landen door elkaar halen. Voorkeuren toedichten die de klant nooit had genoemd. De samenvatting zou de source of truth zijn, en hij verzon dingen.</p>
<p>Toen ontwierpen we een geheugensysteem in drie lagen: kortetermijn, tijdelijk en langetermijn. De tijdelijke laag moest bijhouden welke onderwerpen bij welke bestemming hoorden, welke voorkeuren op welk deel van de reis golden. Op papier elegant. In de praktijk een nachtmerrie. Het vereiste dat de AI complexe relationele context begreep, zoals &ldquo;toen ze zeiden dat het budget flexibel was, bedoelden ze dat voor Thailand, niet voor Vietnam.&rdquo; Dat kon hij niet. Te veel edge cases. Te fragiel.</p>
<p>Wat we uiteindelijk hadden, was simpeler en robuuster. Elk bericht wordt als vector geëmbed in een aparte memory collection in dezelfde database die we voor product search gebruiken. Als de klant zegt &ldquo;die reis met de kookles&rdquo;, draait het systeem een semantic search over alle eerdere berichten in die sessie. Het vindt het relevante bericht, ook als de exacte woorden niet overeenkomen. Misschien noemde Joy het drie beurten eerder een &ldquo;culinaire ervaring&rdquo;. Vector search geeft niets om woordmatching. Het matcht betekenis. Gecombineerd met de laatste 10 uitwisselingen in de prompt voor directe context, hoeft Joy niet voor elke vraag die tool call te doen. Ze heeft het recente gesprek direct bij de hand, en de volledige historie doorzoekbaar wanneer ze die nodig heeft.</p>
<p>Vier pogingen. Elke poging leerde ons iets over wat AI wel en niet kan doen met context. De definitieve versie werkt niet omdat het de slimste opzet is, maar omdat het het eerlijkst is over de beperkingen.</p>
<blockquote>
<p>AI-geheugen is niet één probleem. Het zijn er twee: terugroepen wat er gezegd is, en begrijpen wat er bedoeld is. We hadden twee verschillende systemen nodig om dat op te lossen.</p>
</blockquote>
<h3 id="uiux-the-invisible-work">UI/UX: het onzichtbare werk</h3>
<p>Hier sterven de meeste AI-projecten, zonder dat ze het zelf doorhebben.</p>
<p>Je kunt de meest geavanceerde RAG-pipeline ter wereld hebben. De slimste retrieval. De beste reranking. Maar als de interface traag, log of verwarrend aanvoelt, doet dat er allemaal niet toe. De klant geeft niets om je architectuur. Het gaat ze om hoe het voelt.</p>
<blockquote>
<p>Als je AI slim is maar je interface traag, heb je een genie gebouwd dat opgesloten zit in een kast.</p>
</blockquote>
<p>Hier zijn we diep op ingegaan. En &ldquo;diep&rdquo; betekent tientallen iteraties op dingen die de meeste mensen details zouden noemen. Maar de details zijn het product.</p>
<p><strong>Streaming die natuurlijk voelt.</strong> LLM-antwoorden streamen token voor token over een WebSocket. Maar rauwe streaming geeft flikkerende tekst die schokkerig en onaangenaam is. We bufferen met intervallen van 50ms. Snel genoeg om real-time te voelen, langzaam genoeg om constante re-renders te voorkomen. Het verschil is subtiel, maar enorm voor de ervaren kwaliteit.</p>
<p><strong>De afweging in snelheid.</strong> Laten we eerlijk zijn: een product search met volledig antwoord duurt 20 tot 30 seconden. Dat is lang. We weten dat het lang is. Maar we hebben een bewuste keuze gemaakt: kwaliteit boven snelheid. De brede fetch, de reranking, de semantic matching, elke stap in de pipeline bestaat omdat hij de resultaten beter maakt. Haal er één weg en je krijgt snellere antwoorden met slechtere aanbevelingen. Liever dat Joy 25 seconden werkt en 5 reizen aflevert die echt passen, dan 10 middelmatige matches in 8 seconden.</p>
<p>Dus de vraag werd: hoe zorgen we dat 25 seconden niet aanvoelen als 25 seconden? Daar kwam het UX-werk. Joy laat zien wat ze doet. Producten zoeken. Bestemmingsinformatie opzoeken. Gesprekshistorie checken. Elke tool call is zichtbaar terwijl hij gebeurt. Productkaarten schuiven vervaagd naar binnen terwijl zij nog haar antwoord opbouwt. Zodra het antwoord klaar is, worden alleen de reizen zichtbaar die Joy daadwerkelijk bespreekt. De rest vervaagt. De klant staart nooit naar een leeg scherm. Ze zien het proces ontvouwen. En het blijkt dat ergens naar kijken werken totaal anders voelt dan ergens op wachten.</p>
<p>Een deel van de latency is ook een taalprobleem. Nederlands is slecht vertegenwoordigd in de meeste embedding- en taalmodellen. Het semantische begrip dat in het Engels prachtig werkt, degradeert als je klanten en productdata Nederlands zijn. Dat betekent meer verwerking, zorgvuldiger matchen, meer moeite voor hetzelfde kwaliteitsniveau.</p>
<p><strong>Wat is GPT-NL?</strong> <a href="https://gpt-nl.nl/" data-external>GPT-NL</a> is een Nederlands initiatief om een large language model te bouwen dat specifiek getraind en geoptimaliseerd is voor de Nederlandse taal.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://gpt-nl.nl/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GPT-NL: Een verantwoord taalmodel voor Nederland</span><span class="bookmark__description">In Nederland wordt gewerkt aan de ontwikkeling van een eigen open taalmodel genaamd GPT-NL. Wij gebruiken alleen data om het model te trainen als we die rechtmatig kunnen verkrijgen en bieden zo een verantwoord alternatief op bestaande LLMs. Dit model is essentieel voor het versterken en behouden van onze digitale soevereiniteit.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/bookmarks/01-icon_hu_6ebbb399eb63616a.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GPT-NL</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/bookmarks/01-thumb_hu_6ec85365e1fe875f.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<p>Waar huidige modellen zoals GPT en Claude Nederlands behandelen als een niet-bestaande taal, wil GPT-NL Nederlands een first-class citizen maken in AI. Voor toepassingen zoals de onze, waar zowel de klant als de productdata Nederlands zijn, kan een model dat de taal echt begrijpt de kwaliteit van semantisch zoeken aanzienlijk verbeteren en de extra verwerking verminderen die we nu nodig hebben om dat gat te compenseren. We volgen dit project op de voet.</p>
<p>En dan is er de keuze van het model. De kwaliteit van het gesprek hangt sterk af van welke LLM je gebruikt. We draaien op Sonnet 4.6 als primair model, en het verschil met kleinere of goedkopere modellen is niet subtiel. Het is het verschil tussen een gesprek dat natuurlijk voelt en een dat gerepeteerd aanvoelt. Haiku, de snellere en goedkopere optie, voelt als een downgrade zodra je overschakelt. GPT dient als onze automatische fallback, maar het is een stap terug in gesprekskwaliteit. Dit zijn geen inwisselbare commodity&rsquo;s. Het model dat je kiest bepaalt de ervaring die je klant krijgt.</p>
<p><strong>Directe paginaladingen.</strong> De hele conversatie blijft bewaard in local storage. Als je de pagina ververst of later terugkomt, staat je gesprek er al. Direct. Geen spinner. Geen &ldquo;opnieuw verbinden&hellip;&quot;-melding. De sync met de server gebeurt geruisloos op de achtergrond.</p>
<p><strong>Markdown-normalisatie.</strong> LLM&rsquo;s hebben meningen over opmaak. Claude is gek op vette koppen met emoji. GPT is gek op genummerde lijsten. Wij normaliseren alles server-side. Vet-met-emoji wordt omgezet naar schone koppen. Horizontale lijnen worden gestript. Font weights worden teruggeschroefd. Het resultaat oogt alsof het ontworpen is, niet gegenereerd. De klant moet nooit het gevoel hebben dat hij AI-output zit te lezen.</p>
<p><strong>Sessiecontinuïteit.</strong> Je sessie overleeft paginaverversingen, gesloten browsers en herverbindingen. Kom een dag later terug, je gesprek staat er nog. Je opgeslagen reizen staan er nog. En als een sessie verloopt, krijg je een nette overlay die uitlegt wat er gebeurd is, geen kapotte pagina of een cryptische foutmelding.</p>
<p><strong>De lead flow.</strong> Als een klant klaar is om met een specialist te praten, genereert het systeem een reisvoorstel dat al vooraf is ingevuld met wat het uit de conversatie weet. Besproken bestemmingen, reisvoorkeuren, bekeken producten. Minimale wrijving. Maximale context. De menselijke specialist pakt precies op waar Joy stopte. Dat is geen handoff. Dat is precies het punt. Joy bestaat om dat menselijke gesprek beter te maken, niet om het te vermijden.</p>
<blockquote>
<p>Elk van deze details kostte meerdere iteraties. En geen enkele is zichtbaar voor de klant. Dat is de bedoeling.</p>
</blockquote>
<h3 id="security-an-honest-note">Security: een eerlijke noot</h3>
<p>Ik houd deze sectie kort en eerlijk. Security is niet iets wat je afmaakt. Het is iets waar je continu aan werkt.</p>
<p>We hebben een dubbel guardsysteem. Een input guard vóór de LLM die prompt-injection-pogingen onderschept, en een output guard ná de LLM die credential leaks of system prompt-fragmenten in antwoorden onderschept. Beide zijn deterministisch en voegen vrijwel geen latency toe.</p>
<p>De system prompt is verhard met anti-jailbreak-instructies. Er is rate limiting, botbescherming en IP-beheer voor hardnevig misbruik.</p>
<p>Is het bulletproof? Nee. Niets is dat. Maar het is gebouwd met defense in depth in gedachten, en we itereren erop naarmate nieuwe aanvalspatronen opduiken. Als je een AI-interface bouwt die met het publiek praat en je hebt nog niet nagedacht over input/output guards, dan ligt er een probleem op de loer.</p>
<h3 id="the-bigger-picture-an-extra-channel-not-a-replacement">Het grotere geheel: een extra kanaal, geen vervanging</h3>
<p>Laat me heel duidelijk zijn over wat Joy is en wat ze niet is.</p>
<p>Joy is geen vervanging van onze website. Ze is geen vervanging van telefoon of e-mail. Ze is een extra kanaal. Een nieuwe manier voor klanten om onze producten te ontdekken, naast alles wat er al bestaat. Ze zal nooit een vervanging van menselijk contact zijn. Dat vinden we bij ons bedrijf het belangrijkst. Joy is een middel naar menselijk contact. Ze helpt klanten uitvinden wat ze willen, zodat het gesprek met een van onze specialisten op een compleet ander niveau begint.</p>
<blockquote>
<p>Joy is niet de bestemming. Ze is de weg die leidt naar een echt gesprek met een echt persoon.</p>
</blockquote>
<p>De MCP-server staat al live, wat betekent dat AI-assistenten onze data al programmatisch kunnen bevragen. Als de Web 4.0-visie werkelijkheid wordt en Lisa&rsquo;s AI-assistent zelfstandig op zoek gaat naar haar perfecte Thailand-reis, dan bestaat het endpoint al. Niet omdat we er haastig mee aan de slag zijn gegaan. Omdat we al meer dan een jaar met deze infrastructuur aan het experimenteren zijn.</p>
<p>Joy is niet het eindproduct. Ze is het begin van een kanaalstrategie die zowel mens-tot-AI- als AI-tot-AI-interacties omvat. En ze is een test, een heel serieuze, heel grondige test, of conversationele AI vandaag de dag daadwerkelijk waarde kan toevoegen voor reisklanten.</p>
<p>De resultaten tot nu toe zijn beter dan ik verwachtte. Niet vlekkeloos, niets is dat, maar echt indrukwekkend in hoe natuurlijk de gesprekken voelen en hoe relevant de aanbevelingen zijn. Als je een klant een vaag idee ziet beschrijven en Joy komt met reizen die daadwerkelijk passen, dan besef je: dit is geen gimmick. Het werkt. En het werkt dankzij alles wat eronder ligt.</p>
<h3 id="so-what-are-we-trying-to-solve-again">Dus, welk probleem lossen we ook alweer op?</h3>
<p>Ik begon met deze vraag omdat dit de vraag was die alles heeft geleid wat we bouwden. Niet &ldquo;hoe gebruiken we AI?&rdquo; Niet &ldquo;wat doen onze concurrenten?&rdquo; Niet &ldquo;hoe komen we innovatief over?&rdquo;</p>
<p>Alleen: welk probleem lossen we op, en werkt deze oplossing daadwerkelijk?</p>
<p>We bouwden Joy om daarachter te komen. Met echte architectuur. Echte data. Echte klanten. En de bereidheid om &ldquo;dit is nog niet klaar&rdquo; te zeggen als dat zo zou blijken.</p>
<p>Als je nadenkt over het toevoegen van AI aan je product, begin daar. Niet met de tool. Niet met de pitch van een leverancier. Niet met de druk om bij te blijven.</p>
<p><strong>Begin met de vraag. Bouw vanuit het antwoord.</strong></p>
<p>Je klanten verdienen beter dan een FAQ met een gezicht. Ze verdienen een echt gesprek. En uiteindelijk een echt mens. Zorg dat je AI ze daar naartoe leidt, en niet ervandaan.</p>
<p>Als je <a href="https://www.allsubjectsmatter.nl/nl/nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/" data-preview>Deel 1</a> nog niet gelezen hebt: daar begint het verhaal. Waarom we dit bouwden, wat we leerden van ons mislukken in 2019, en waarom je hier niet komt met een plugin.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/"><span class="bookmark__body"><span class="bookmark__title">Can a conversation be a travel channel? Part 1: We built one to find out.</span><span class="bookmark__description">We built a full AI travel interface from scratch, not to say we do AI, but to test whether a conversation can actually be a meaningful channel for personalized travel. This is what it took, why a plugin will never get you there, and why most AI chatbots are just expensive search bars.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/bookmarks/02-icon_hu_8a3f1fd6616d7152.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/bookmarks/02-thumb_hu_bbbba9d6b5c5a7aa.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
    <item>
      <title>Kan een AI-conversatie een reiskanaal zijn? Deel 1: we bouwden er een om erachter te komen.</title>
      <link>https://www.allsubjectsmatter.nl/nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/</link>
      <guid isPermaLink="false">69afffb35418c000015caaa6</guid>
      <pubDate>Tue, 10 Mar 2026 13:46:34 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[AI and LM]]></category>
      <description>&lt;![CDATA[We bouwden een volledige AI-reisinterface vanaf nul. Niet om te kunnen zeggen dat we &#34;met AI bezig zijn&#34;, maar om te testen of een gesprek echt een betekenisvol kanaal kan zijn voor gepersonaliseerde reizen. Dit was ervoor nodig, waarom een plugin je daar nooit brengt, en waarom de meeste AI-chatbots gewoon dure zoekbalken zijn.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>We bouwden een AI-aangedreven reisinterface vanaf nul. Geen chatbot. Geen widget. Geen GPT-wrapper met een smiley op een website geplakt. Een volledige conversationele interface met een eigen vector database, een eigen retrieval pipeline, een eigen geheugensysteem en real-time streaming over WebSockets. Niet om iets te vervangen. Om te testen of conversationele AI er echt klaar voor is om een betekenisvol kanaal voor reizen te zijn. Dit is wat we ontdekten, wat het kostte, en waarom we denken dat het meeste van wat er nu is ruis is.</p>
<hr>
<p>Dit is een verhaal in twee delen. Dit eerste deel gaat over het waarom: welk probleem we oplossen, wat we leerden van eerdere mislukkingen, en waarom een plugin je hier nooit zal brengen. <a href="https://www.allsubjectsmatter.nl/nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/" data-preview>Deel 2</a> gaat dieper in op de architectuur: de RAG pipeline, het geheugensysteem, de UI/UX-beslissingen en de technische afwegingen die we maakten.</p>
<h2 id="what-are-you-actually-trying-to-solve">Wat probeer je eigenlijk op te lossen?</h2>
<p>Dat is de vraag waar ik steeds op terugkom. Elke keer als een bedrijf zijn nieuwe &ldquo;AI-powered&rdquo; feature aankondigt. Elke keer als iemand me vraagt wat wij met AI doen. Elke keer als een leverancier weer een plugin komt pitchen.</p>
<p>Het is op dit moment de belangrijkste vraag in tech. En bijna niemand stelt hem.</p>
<p>In plaats daarvan werkt de hele industrie achterstevoren: men begint met &ldquo;we moeten iets met AI doen&rdquo; en gaat dan op zoek naar een plek om het neer te zetten. Bedrijven horen &ldquo;AI&rdquo; en horen &ldquo;innovatie&rdquo; en denken dat ze achterlopen als ze niet nú iets doen.</p>
<blockquote>
<p>De druk om &ldquo;iets met AI te doen&rdquo; is luider geworden dan de vraag wat je eigenlijk wilt bereiken.</p>
</blockquote>
<p>En wat gebeurt er dan? Bedrijven grijpen de meest zichtbare, meest verkoopbare toepassing die ze kunnen vinden: een chatbot. Op de website plakken. Een naam en een vriendelijk gezichtje geven. Verbinden met een kennisbank. Klaar. &ldquo;Wij doen nu AI.&rdquo;</p>
<p>Het probleem: de integratie is altijd beperkt. Sommigen koppelen aan een productdatabase. Sommigen halen live beschikbaarheid op. Maar als plugin is er een plafond aan hoe diep die integratie gaat. Hij werkt altijd met de data die hij krijgt, in het formaat dat de API levert, met de context die de leverancier voldoende vond. Het resultaat is een interactie die aan de oppervlakte conversationeel oogt, maar fundamenteel beperkt blijft in wat hij kan.</p>
<p>En omdat het een plugin is, kan het nooit volledig naadloos zijn. Het blijft altijd een aparte laag bovenop je website, geen onderdeel ervan. Een andere designtaal. Een ander interactiemodel. Een andere ervaring. De klant voelt dat, ook al kan hij het niet onder woorden brengen. Het is het verschil tussen een kamer die als geheel is ontworpen en een kamer waar iemand een kiosk in de hoek heeft gezet.</p>
<blockquote>
<p>Een slimmere poortwachter is nog steeds een poortwachter. Je klanten wilden een deur.</p>
</blockquote>
<p>We zijn zelf deze weg gegaan. In 2019 bouwden we de <a href="https://www.allsubjectsmatter.nl/nl/nl/how-333travel-is-revolutionizing-the-travel-industry/" data-preview>eerste versie van Joy</a> op Flow.ai. Een intent-gebaseerde chatbot die natuurlijke taal begreep, reisvoorkeuren uit één zin kon halen en leads genereerde voor onze specialisten. Het werkte. Technisch gezien. Maar de klantbetrokkenheid was te laag om hem in leven te houden. Begin 2022 hebben we Joy uit dienst genomen.</p>
<p>De les was niet dat de technologie slecht was. De les was dat mensen hebben geleerd chatbots te wantrouwen. En terecht. Jarenlang hebben bedrijven chatbots gebruikt als barrière om contact te maken, ook toen ze het tegendeel beweerden. Klanten weten dat. Ze hebben het meegemaakt. Na drie foute antwoorden denk je niet &ldquo;even opnieuw proberen.&rdquo; Je denkt &ldquo;hoe kom ik bij een echt mens?&rdquo; Sommige mensen doen niet eens de moeite. Ze zien een chatbelletje met een gezichtje en hun eerste reactie is: weg ermee. Niet omdat ze ongeduldig zijn, maar omdat de ervaring hun heeft geleerd dat het tijdverspilling is.</p>
<p>Dat is geen technologieprobleem. Dat is een vertrouwensprobleem. En dat los je niet op door hetzelfde formaat slimmer te maken.</p>
<p>Daar komt bij dat de interface van een chatbot-plugin simpelweg beperkend is. Een klein venster in de hoek van je scherm. Een paar regels tekst. Misschien een knop of twee. Daarin kun je geen rijke productkaarten tonen. Je kunt geen kaart van een reis laten zien. Je kunt iemand niet laten browsen, vergelijken, reizen bewaren voor later. Het formaat dicteert de ervaring, en het formaat is een doos.</p>
<p>Die ervaring is precies waarom we geen nieuwe chatbot hebben gebouwd. Geen slimmere. Geen betere. Iets fundamenteel anders. Een volledige conversationele interface, vanaf nul gebouwd, ontworpen om te inspireren. Waar iemand bestemmingen kan verkennen, reizen kan ontdekken waarvan hij het bestaan niet kende, en waar hij — als hij er klaar voor is — na een gesprek dat echt goed voelde in contact komt met een specialist. Niet na drie mislukte pogingen om langs een poortwachter te komen.</p>
<p>De echte vraag was nooit &ldquo;hoe voegen we AI toe aan onze website?&rdquo; De echte vraag is: waar creëert AI echt waarde die eerder niet mogelijk was? Dat zijn we gaan testen.</p>
<h2 id="why-i-built-what-i-built">Waarom ik bouwde wat ik bouwde</h2>
<p>Ik heb dit niet gebouwd omdat ik denk dat websites dood zijn, of omdat ik wilde kunnen zeggen dat we &ldquo;iets met AI doen&rdquo;. Ik heb eerder geschreven over <a href="https://www.allsubjectsmatter.nl/nl/nl/lets-stop-talking-about-ai-and-start-solving-real-problems/" data-preview>hoeveel ik een hekel heb aan die mentaliteit</a>.</p>
<p>Ik heb dit gebouwd vanwege een vraag die ik al meer dan een jaar toets: is conversationele AI er echt klaar voor om een echt kanaal te zijn?</p>
<p>In mijn <a href="https://www.allsubjectsmatter.nl/nl/nl/web-4-0-and-the-autonomous-web-the-rise-of-ai-driven-customers/" data-preview>Web 4.0-post</a> schreef ik over het autonome web, een wereld waarin AI-assistenten niet alleen informatie ophalen, maar optreden als onafhankelijke digitale consumenten namens hun mensen. Waar Lisa tegen haar AI-assistent zegt &ldquo;zoek een reis van twee weken naar Thailand&rdquo; en de AI via MCP-interfaces contact opneemt met reisorganisaties, onderhandelt, verfijnt en opties presenteert. Zonder dat Lisa ook maar een browser opent.</p>
<p>Dat klinkt misschien alsof de mens uit het plaatje verdwijnt. Dat is niet zo. De techniek maakt volledige autonomie mogelijk. Maar dat betekent niet dat we de mens willen weghalen. Het betekent dat de klant mag kiezen. En onze default is en blijft: naar een mens leiden. Daar later meer over.</p>
<p>Die toekomst komt. Misschien niet morgen, maar hij komt. En als je een reisorganisatie bent die zichzelf serieus neemt, kun je er maar beter op voorbereid zijn.</p>
<p>Dus bouwde ik Joy, een AI-reisassistent voor 333travel. Niet als productlancering, niet als marketingstunt, maar als een serieuze test. Kunnen we een conversatie-interface bouwen die écht waarde toevoegt? Die onze producten begrijpt? Die minder hallucineert? Die een echt nuttige ervaring neerzet als extra kanaal naast onze website, telefoon en e-mail?</p>
<blockquote>
<p>Ik heb dit niet gebouwd om onze website te vervangen. Ik heb dit gebouwd om een vraag te beantwoorden: kan een gesprek een betekenisvol kanaal zijn voor gepersonaliseerd reizen?</p>
</blockquote>
<h2 id="what-this-actually-is">Wat het eigenlijk is</h2>
<p>Laat me concreet zijn over wat Joy doet:</p>
<p>Joy zit bovenop onze volledige productdatabase. Elke rondreis, elk hotel, elke excursie, elke bestemming, allemaal als vectoren geëmbed in een eigen database. Als een klant zegt &ldquo;ik wil iets avontuurlijks in Zuidoost-Azië, zo&rsquo;n twee weken, niet te toeristisch,&rdquo; dan gaat Joy niet keyword-matchen tegen een PDF. Ze doorzoekt semantisch honderden producten, rerankt de resultaten op werkelijke relevantie en komt met opties die echt aansluiten bij de intentie.</p>
<p>En ze onthoudt het gesprek. Niet alleen het laatste bericht, de hele thread. Ze weet welke reizen besproken zijn, wat bewaard is, wat verworpen. Als je zegt &ldquo;die reis die je eerder noemde met de kookles,&rdquo; snapt ze wat je bedoelt, ook al noemde ze het zelf een &ldquo;culinaire ervaring.&rdquo;</p>
<p>Maar hier zit het cruciale verschil met elke willekeurige chatbot: ze weet niet alleen iets van reizen in het algemeen. Ze kent ónze reizen. Onze specifieke producten. Onze specifieke reisschema&rsquo;s. Onze bestemmingen die ons team zelf heeft bezocht. Dat kun je niet zomaar met een plugin erop plakken, hoe slim de LLM erachter ook is.</p>
<p>En op het moment dat je menselijk contact wilt? Dan geeft Joy de volledige context door aan een echte reisspecialist die precies weet wat je hebt verkend. Geen &ldquo;kun je je vraag nog eens omschrijven.&rdquo; Geen koude overdracht. Volledige continuïteit.</p>
<blockquote>
<p>Joy vervangt gesprekken met specialisten niet. Ze maakt ze beter.</p>
</blockquote>
<p>Wat ze (nog) niet doet: klantenservicevragen, luchtvaartinformatie of support na boeking afhandelen. Dit is puur een kanaal voor productontdekking en inspiratie. Daar komen we nog wel. Maar we gaan niet doen alsof ze dingen kan die ze niet kan. En eerlijk gezegd: als iemand bij de klantenservice terechtkomt, is er al een probleem. Dat is het slechtst denkbare moment om een machine tussen jou en je klant te zetten. Dan moet je er zelf zijn. Persoonlijk. Dus we zijn nog steeds aan het discussiëren over wat we haar daar laten doen. De nuance zit altijd in de details.</p>
<h2 id="why-you-cant-get-here-with-a-plugin">Waarom je hier niet komt met een plugin</h2>
<p>Wat wij gebouwd hebben, bereik je niet door een plugin te installeren. Niet met ChatGPT. Niet met een of andere standaard &ldquo;AI-widget.&rdquo; Niet met een SaaS-tool die belooft &ldquo;je website in minuten slimmer te maken.&rdquo;</p>
<p>En dat komt niet doordat de AI-modellen niet goed genoeg zijn. De modellen zijn prima. Het probleem zit in alles eromheen.</p>
<p>Het hele systeem draait op data die wij zelf bezitten, structureren en beheersen. Elke reis in onze database is door ons eigen team bezocht. Elk reisschema is onze eigen creatie. Elk stuk bestemmingenkennis leeft in onze eigen systemen. Dit is geen data die we van een derde partij licentiëren of uit een brochurecatalogus trekken.</p>
<p>Dat betekent dat we het kunnen embedden. Dat we het precies zo kunnen structureren als we nodig hebben voor semantisch zoeken. Dat we het kunnen bijwerken op het moment dat er iets verandert. Wij beheersen de volledige pipeline, van ruwe productdata naar vector embedding naar retrieval naar antwoord.</p>
<blockquote>
<p>Je kunt geen slimme interface bouwen bovenop bergen domme data.</p>
</blockquote>
<p>Als je productdata in andermans systeem leeft, als je reizen uit een catalogus doorverkoopt, als je &ldquo;database&rdquo; een verzameling PDF&rsquo;s van touroperators is, dan ben je afhankelijk van welke structuur iemand anders heeft bedacht. Je kunt geen intelligentie bouwen op data die je niet begrijpt of beheerst.</p>
<p>En veel bedrijven starten niet alleen vanaf nul, ze starten met een achterstand. Hun bestaande systemen zijn al onvoldoende. De booking engine is verouderd. De productdata ligt verspreid over spreadsheets en PDF&rsquo;s. De customer journey heeft overal gaten. En in plaats van die fundamenten te fixen, vinden ze het slim om er een AI-chatbot bovenop te leggen. Alsof intelligentie bovenop disfunctie iets functioneels oplevert. Doet het niet. Het maakt de disfunctie alleen moeilijker te diagnosticeren.</p>
<blockquote>
<p>Je lost geen probleem op. Je versiert het.</p>
</blockquote>
<p>Wat wij gebouwd hebben, vereiste drie dingen:</p>
<p><strong>Onze eigen data.</strong> Elke reis is van ons. Bezocht door ons team. Gestructureerd in onze database. Embedded in onze vector store. Je kunt geen semantic search bouwen over data die je niet zelf structureert.</p>
<p><strong>Onze eigen systemen.</strong> Onze eigen backend, frontend, deployment pipeline. Toen we een dual reranker nodig hadden, bouwden we die. Toen we semantic conversation memory nodig hadden, bouwden we het. Toen we een MCP server nodig hadden voor Web 4.0-readiness, bouwden we die. Geen goedkeuring van een vendor nodig. Geen feature requests. Geen afhankelijkheid van andermans roadmap.</p>
<p><strong>De hele keten in eigen hand.</strong> Van het moment dat een klant de chat opent tot het moment dat ze met een specialist praten, is elke stap van ons. De AI kent onze producten omdat we de producten zelf hebben gemaakt. De specialist kent het gesprek omdat ons systeem het aan hen doorgeeft.</p>
<blockquote>
<p>De AI is de laatste 10% van het werk. De eerste 90% is een bedrijf bouwen dat het kan dragen.</p>
</blockquote>
<p>In <a href="https://www.allsubjectsmatter.nl/nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/" data-preview>deel 2</a> laat ik zien hoe het onder de motorkap echt werkt. De architectuur, de search pipeline, het geheugensysteem dat vele pogingen kostte om goed te krijgen, en waarom 25 seconden wachten aanvoelt als 5 als je het goed ontwerpt.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/the-technical-architecture-behind-a-conversational-travel-channel/"><span class="bookmark__body"><span class="bookmark__title">Can a conversation be a travel channel? Part 2: The technical architecture</span><span class="bookmark__description">The technical deep-dive into how we built Joy: a RAG pipeline with vector search and reranking, a memory system that took four iterations, and the UI/UX decisions that make a 25-second response feel instant.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/bookmarks/01-icon_hu_8a3f1fd6616d7152.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/bookmarks/01-thumb_hu_cb9ae8a41b04a42d.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
    <item>
      <title>Ik vibecodede een app en dit heb ik ervan geleerd.</title>
      <link>https://www.allsubjectsmatter.nl/nl/i-vibecoded-an-app-and-this-is-what-i-learned/</link>
      <guid isPermaLink="false">6911a93d8225830001cea697</guid>
      <pubDate>Mon, 10 Nov 2025 11:37:31 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[AI and LM]]></category><category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Ik bouwde een &#34;productie&#34;-app met vibecoding. 97.089 regels gegenereerd. Slechts 25.056 kwamen in productie. Vier complete refactors. De AI faalde niet door slechte prompts. Het faalde omdat AI dingen vergeet, context kwijtraakt en dubbele logica creëert, hoe goed je het ook stuurt.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/i-vibecoded-an-app-and-this-is-what-i-learned/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Ik heb een &ldquo;productie&rdquo;-app gebouwd met vibecoding. 97.089 regels gegenereerd. Slechts 25.056 regels haalden productie. Vier complete refactors. De AI faalde niet door slechte prompts. Het faalde omdat AI dingen vergeet, context kwijtraakt en duplicaatlogica creëert, hoe goed je het ook stuurt. Vibecoding gaat er niet om AI voor je te laten bouwen. Het gaat erom dat je je systeem goed genoeg kent om de ruis te filteren. Zonder die kennis ship je alleen maar gissingen.</p>
<hr>
<h2 id="what-vibecoding-actually-means"><strong>Wat vibecoding eigenlijk betekent</strong></h2>
<p>Mensen denken dat vibecoding betekent dat AI je app bouwt terwijl jij toekijkt. Reality check: dat is helemaal niet het geval als je een daadwerkelijk werkend product nodig hebt met elke keer het verwachte resultaat.</p>
<p>Vibecoding betekent nog steeds dat jij de controle hebt. Je prompt met precisie. Je experimenteert. Je stuurt. Je filtert. Het blijft handwerk, alleen met andere hefboomwerking.</p>
<blockquote class="pull-quote"><p>&ldquo;A fool with a tool is still a fool&rdquo;</p>
</blockquote>
<p>En als je niet weet wat je doet? Dan is het resultaat hetzelfde als altijd: een zooitje. Vibecoding zonder de juiste kennis is gewoon fout. En het grootste probleem met AI is dat het maar blijft produceren: code na code na code. Het doet gewoon wat je vraagt. Het is daar niet kritisch over. Dus als je niet weet wat je doet, spreek je jezelf de hele tijd tegen met geprogrammeerde functionaliteit die nooit samenwerkt binnen je app.</p>
<p>AI is koning in fallbacks. Ik had voor elke mislukte API-call 5 verschillende fallbacks gekregen. Zouden we niet gewoon de juiste manier aan de praat moeten krijgen in plaats van fallbacks bouwen? AI is ook lui: het onderdrukt liever notices dan dat het ze oplost. Zeker als het niet in één keer lukt.</p>
<h2 id="i-built-a-production-app-with-ai-but-not-because-of-ai"><strong>Ik bouwde een productie-app met AI (maar niet dankzij AI)</strong></h2>
<p>Ik heb Windsurf gebruikt om een complete applicatie te bouwen. Geen menselijke developers. Geen code die ik direct heb geschreven. We gebruiken Sonnet 4.5 als onze kennisbron.</p>
<p>Het resultaat na veel frustratie: een volwaardig productiespel met backend-logica, login-flows, async data writes, mail queues, admin security, monitoring en cartesische algoritmes die updaten op basis van gebruikersinteractie.</p>
<p>De AI heeft alles geschreven. Ik heb geen enkele regel code aangeraakt. Zelfs de simpelste CSS-wijziging liet ik aan AI over.</p>
<p>Maar wat niemand je vertelt: ik heb het hele project vier keer compleet moeten refactoren. En niet omdat ik slecht promptte. Ik gebruikte KISS-principes, DRY-standaarden, PSR-richtlijnen. We schreven vooraf beslissingen uit en maakten implementatiedocumenten. We gebruikten richtlijnen voor de talen die we gebruikten.</p>
<p>Uiteindelijk maakte het niets uit.</p>
<p>AI vergeet. Het verliest context. Het begint duplicaatfuncties te maken zonder door te hebben dat het die logica al drie files terug schreef. Hoe complexer je project wordt, hoe meer het afdrijft. En hoe vaker het niet in één keer goed gaat, hoe meer AI denkt backwards compatibility te moeten handhaven, waardoor je code nog complexer wordt. Het maakt niet uit hoe je het de eerste keer prompt, of met project rules.</p>
<blockquote>
<p>Na vier complete refactors leerde ik: AI vergeet de context niet. Het heeft die nooit gehad ;).</p>
</blockquote>
<p>De enige reden dat de definitieve versie werkt, is dat ik precies wist wat ik aan het bouwen was. Ik had het systeem in mijn hoofd. Ik zag wanneer AI zichzelf herhaalde. Ik wist wanneer ik moest stoppen, bijsturen of opnieuw beginnen. Ik controleerde elke file op duplicaat fallback-data, duplicaten enzovoort. Ik gebruikte ook het zeer handige npm-package knip.</p>
<p><a href="https://www.npmjs.com/package/knip" data-external>https://www.npmjs.com/package/knip</a></p>
<p>Je kunt niet iets de productie in vibecoden zonder je product te begrijpen. Punt. En je moet accepteren dat bugfixen betekent dat je op AI leunt om het op te lossen zonder iets anders te breken. Veel succes daarmee&hellip;</p>
<h2 id="the-data-tells-the-rest-of-our-story"><strong>De data vertelt de rest van ons verhaal</strong></h2>
<p>Tijdens ons vibecoden produceerde Windsurf:</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/i-vibecoded-an-app-and-this-is-what-i-learned/images/Screenshot-2025-11-10-at-09.28.05_hu_8c902f8cc6fdc18d.webp" alt="" width="560" height="442" data-zoom-src="https://www.allsubjectsmatter.nl/nl/i-vibecoded-an-app-and-this-is-what-i-learned/images/Screenshot-2025-11-10-at-09.28.05_hu_f69c6e1b7aae2732.webp" loading="lazy" decoding="async">
  </picture></figure>

<ul>
<li>97.089 regels code gegenereerd</li>
<li>2.925 prompts verstuurd</li>
<li>5.693 credits gebruikt</li>
<li>15.734 tool calls (waaronder 7.000 code-edits)</li>
</ul>
<p>Wat er daadwerkelijk in productie is gegaan:</p>
<ul>
<li>25.056 regels code</li>
</ul>
<p>Meer dan 70.000 regels zijn geschrapt, vervangen of gerefactord.</p>
<p>Nu denk je misschien: dan moet je AI dus gewoon &ldquo;beter sturen&rdquo; of &ldquo;preciezer prompten&rdquo;. Dat is niet het punt dat ik wilde maken.</p>
<p>Het punt is: <strong>je kunt AI-gebral op dit moment niet voorkomen.</strong> Hoe goed je ook prompt. Hoe gestructureerd je aanpak ook is. AI overproduceert. Het genereert redundante code. Het raakt de tel kwijt van wat het al gebouwd heeft. Het creëert ruis. AI is gebouwd om altijd een vorm van AI-verbaal diarree te produceren.</p>
<p>Die 70k+ regels verspilde output? Dat is geen bug. Dat is de huidige staat van vibecoding.</p>
<p>En als je je systeem niet goed genoeg kent om te herkennen wat troep is en wat niet, ship je alles en kun je zelfs met AI niet meer debuggen. Of erger: je besteedt persoonlijk weken aan het debuggen van een codebase die je niet snapt.</p>
<p>De kosten per 1000 credits liggen rond de 35 dollar. De ontwikkeling van dit spel kostte me 210 dollar. Elk bedrijf dat Large Language Models levert verliest geld. De code die ik produceerde had makkelijk 210 x 10 = 2.100 dollar moeten kosten, minimaal. Ik vraag me af of ze dan überhaupt geld verdiend hadden.</p>
<p>Los van het geld: is dit onze kostbare elektriciteit echt waard? Ik ben benieuwd hoe haalbaar dit is als je de echte prijs betaalt.</p>
<h2 id="what-people-get-wrong"><strong>Wat mensen verkeerd begrijpen</strong></h2>
<p>AI is geen magie. Het is een &ldquo;slimme&rdquo;, snelle assistent. Maar hij begrijpt niet wat er voor jouw bedrijf telt. Hij heeft geen grip op edge cases, constraints, risico&rsquo;s, performance-implicaties of security-kwetsbaarheden.</p>
<p>Dat is jouw werk!</p>
<p>In mijn geval hielp AI om iets echts te bouwen. Maar de AI beveiligde het adminpaneel niet. Die mail queue-logica ontwierp hij niet. Hij besliste niet wanneer async writes gedebounced moesten worden. Dat deed ik. Door kritisch te zijn en de juiste vragen te stellen dankzij bouwervaring van vroeger.</p>
<p>Zonder die richting was het hele ding in elkaar gestort.</p>
<p>En ik zie dit patroon overal. Teams die AI-producten bouwen zonder ownership of richting, alleen maar om te kunnen zeggen dat ze &ldquo;iets met AI gedaan hebben&rdquo;. Dat is oppervlakkig. Dat is broos. Het schaalt niet.</p>
<blockquote class="pull-quote"><p><em>&ldquo;You&rsquo;re not building products. You&rsquo;re laundering prompts into deployments&rdquo;</em></p>
</blockquote>
<p>Hoe vaak hoor je niet van bovenaf: we moeten iets met AI doen. We raken achter. Achter op wat?</p>
<h2 id="the-customer-service-trap"><strong>De customer service-val</strong></h2>
<p>Dan is er nog de hype rond AI voor klantenservice. Bedrijven beweren dat het perfect is voor 24/7 support, snellere antwoorden, minder mensen nodig.</p>
<p>Maar als je een bot nodig hebt om met je klanten te praten, ligt je echte probleem waarschijnlijk ergens anders. AI repareert kapotte processen niet. Het verstopt ze. Tijdelijk.</p>
<p>Als je minder menselijk contact als winst ziet, heb je al verloren. Als het beantwoorden van vragen lang duurt, moet je misschien ergens anders kijken waarom mensen zoveel vragen hebben, en dat eerst verbeteren.</p>
<h2 id="the-actual-point"><strong>Het eigenlijke punt</strong></h2>
<p>Vibecoding is gewoon weer een tool. Meer niet. Wel een heel give tool in de juiste handen, maar toch een tool. En vooral goed voor prototyping. Maar met een flinke dosis kennis kun je er misschien standalone apps mee bouwen die écht werken, die je op z&rsquo;n minst zelf kunt testen en waarvan het resultaat op een transparante manier te herleiden is naar de geschreven code.</p>
<p>Het vervangt geen denken. Het zou je denken moeten versterken, en het zou je niet minder moeten laten nadenken.</p>
<p>Goed gebruikt kan het krachtig zijn. Maar je moet nog steeds weten wat je wilt bouwen. Je moet de structuur ownen. Je moet de ruis filteren. Je moet AI-geklets herkennen als je het ziet. Je moet het proces leiden.</p>
<p>Anders bouw je geen prototype. Dan genereer je alleen gokken.</p>
<p>De toekomst is niet AI-first. De bedrijven die ertoe doen blijven human-first. Zij begrijpen het probleem voordat ze een tool kiezen. En ze blijven de enige vraag stellen die telt, voordat ze AI te hulp roepen:</p>
<p><strong>Wat lossen we nu eigenlijk op?</strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Stop met praten over AI (en los echte problemen op)</title>
      <link>https://www.allsubjectsmatter.nl/nl/lets-stop-talking-about-ai-and-start-solving-real-problems/</link>
      <guid isPermaLink="false">68d2468e330a1600017fa3d5</guid>
      <pubDate>Tue, 23 Sep 2025 11:59:39 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[AI and LM]]></category>
      <description>&lt;![CDATA[In dit artikel behandel ik waarom AI een tool is en geen strategie; waarom hallucinaties in de huidige generatieve modellen wiskundig onvermijdelijk zijn; en waarom echte vooruitgang begint met een degelijke architectuur, ownership en helder nadenken — en niet met hype najagen.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/lets-stop-talking-about-ai-and-start-solving-real-problems/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Artificial Intelligence is er om te blijven, daar gaat het hier niet over.</p>
<p>Het probleem is dat <strong>iedereen over AI praat</strong>, maar dat weinig mensen nadenken over hoe je het écht goed inzet. Ergens onderweg is &ldquo;wij gebruiken AI&rdquo; een marketingterm geworden in plaats van een technische beslissing. Iedereen wil meedoen met de AI-golf, anders hoor je er niet bij. Als je de big tech moet geloven, die het iedereen door de strot probeert te duwen.</p>
<p>Maar als je een klein stapje terugneemt, valt iets vreemds op:</p>
<blockquote>
<p>Bedrijven lossen met AI problemen op die ze eigenlijk nooit hadden mogen hebben.</p>
</blockquote>
<p>Slecht ontworpen systemen, ontbrekend ownership en spaghetti-infrastructuur worden nu &ldquo;opgelost&rdquo; door er LLM&rsquo;s bovenop te gooien. Met als gevolg dat systemen nog moeilijker te onderhouden zijn, duurder worden en lastiger te begrijpen.</p>
<p>Deze blog gaat niet over AI afkraken.<br>
Het gaat over <strong>helder nadenken, technisch ownership</strong> en de juiste tools voor de klus gebruiken.</p>
<h2 id="openai-investigation-research-paper">Onderzoeksrapport OpenAI</h2>
<p>Recent onderzoek van OpenAI en Georgia Tech benadrukt een kernwaarheid die vaak genegeerd wordt: <strong>AI-hallucinaties, die momenten waarop modellen dingen verzinnen, zijn geen bugs. Ze zijn een wiskundig gevolg van hoe deze systemen gebouwd zijn.</strong></p>
<p>Zelfs met perfecte trainingsdata beloont de huidige architectuur van generatieve AI zelfverzekerde antwoorden boven het toegeven van onzekerheid. Dat betekent dat hallucinaties niet alleen vaak voorkomen, ze zijn <em>onvermijdelijk</em>, tenzij we fundamenteel veranderen hoe we deze modellen trainen en evalueren.</p>
<blockquote>
<p>Zoals de onderzoekers het formuleren: &ldquo;Hallucinations are mathematically inevitable.&rdquo; (<a href="https://arxiv.org/pdf/2509.04664" data-external>bron</a>)</p>
</blockquote>
<h2 id="how-people-abuse-ai-to-solve-the-wrong-problems">Hoe mensen AI misbruiken om de verkeerde problemen op te lossen</h2>
<p>Laten we een stap terugnemen.</p>
<p>Als je $1.200 per maand betaalt aan GPT-4 om e-mailtekst te parsen, telefoonnummers te extraheren, JSON te herformatteren en strings naar hoofdletters te zetten… dan innoveer je niet. Dan besteed je gezond verstand uit aan een dure black box.</p>
<p>Ja, iemand deed dit recentelijk, en besefte te laat wat het kostte. Die persoon stapte toen over op GPT-4o-mini en bracht de rekening terug naar $200… Maar ook dan is het nog steeds een kutoplossing:</p>
<blockquote>
<p>Die taken hadden helemaal geen AI nodig. Gewoon basale softwarekennis.</p>
</blockquote>
<p>Dit is geen uitzondering. Het gebeurt helaas vaak.</p>
<p>Mensen denken dat AI hun oplossing voor alles is, terwijl het alleen de incompetentie van de persoon blootlegt, want:</p>
<ul>
<li>Het verhoogt de kosten</li>
<li>Het verbergt wat er werkelijk gebeurt</li>
<li>Het introduceert verborgen afhankelijkheden</li>
<li>Het maakt systemen vrijwel onmogelijk te debuggen en onderhouden</li>
</ul>
<p>AI wordt gebruikt om een <em>gebrek aan systeemontwerp te maskeren</em>, niet om het te verbeteren.</p>
<h2 id="ai-as-a-natural-language-interface-powerful-but-misused">AI als natural language interface, krachtig maar verkeerd gebruikt</h2>
<p>Maar laten we eerlijk zijn: LLM&rsquo;s zijn écht krachtig als je ze goed inzet.<br>
Ze laten je in natuurlijke taal beschrijven wat je wilt, en geven dan een resultaat, zonder dat je de onderliggende logica hoeft te begrijpen.</p>
<p>Dat maakt AI een fantastische tool voor:</p>
<ul>
<li>Prototyping</li>
<li>Werken met grote kennisbanken</li>
<li>Het samenvatten van ongestructureerde data</li>
<li>Fuzzy matching op grote schaal</li>
</ul>
<blockquote>
<p>LLM&rsquo;s zijn in wezen een nieuw soort interface, net zoals de command line, de GUI of API&rsquo;s dat eerder waren.</p>
</blockquote>
<p>Maar ze zijn geen <em>magie</em>. En zeker niet gratis. En bijna altijd is het niet de beste manier om een probleem op te lossen.</p>
<p>Hoe kritischer de functie, hoe belangrijker het wordt om te snappen:</p>
<ul>
<li>Wat wordt er uitgevoerd?</li>
<li>Waar stroomt de data naartoe?</li>
<li>Wie is eigenaar van de output?</li>
<li>Wat zijn de werkelijke kosten?</li>
</ul>
<h2 id="conversational-ai-the-modern-chatbot-but-still-a-chatbot">Conversational AI, de moderne chatbot, maar nog steeds een chatbot</h2>
<p>Er <em>is</em> een weg vooruit voor conversational AI. Alleen is dat niet de revolutionaire sprong die mensen ervan verwachten, met zelfdenkende systemen die niet hallucineren en altijd het juiste antwoord geven.</p>
<p>Ja, LLM&rsquo;s kunnen slimmere, menselijkere antwoorden geven dan vóórdat LLM&rsquo;s mainstream werden.<br>
Ja, ze kunnen supportflows automatiseren die voorheen veel complexer waren om te scripten.</p>
<p>Maar laten we onszelf niets wijsmaken:</p>
<blockquote>
<p>Mensen willen nog steeds <strong>echt, menselijk contact</strong> met je bedrijf, zeker als er iets misgaat.</p>
</blockquote>
<p>Als je je volledige klantenservice uitbesteedt aan een AI-assistent, stuur je een duidelijke boodschap (in mijn ogen de verkeerde):</p>
<ul>
<li>&ldquo;Het kan ons niet genoeg schelen om zelf met je te praten.&rdquo;</li>
<li>&ldquo;Jij bent de tijd van een echt persoon niet waard.&rdquo;</li>
<li>&ldquo;Wij verkopen alleen ons product. Kom alsjeblieft niet bij ons zeuren.&rdquo;</li>
</ul>
<p>Op korte termijn bespaart dat misschien kosten.<br>
Maar op lange termijn? Dan vernietig je het vertrouwen in je merk. Zeker omdat mensen doorgaans contact opnemen met de klantenservice als:</p>
<ul>
<li>Ze gefrustreerd zijn</li>
<li>Ze iets niet begrijpen</li>
<li>Ze een klacht hebben</li>
</ul>
<p>Dat is <strong>niet</strong> het moment om ze een chatbot voor te schotelen, hoe &ldquo;slim&rdquo; die ook klinkt.</p>
<blockquote>
<p>Goede klantenservice is een relatie, geen kostenpost.</p>
</blockquote>
<p>Dus ja, gebruik conversational AI om je teams te ondersteunen. Maar laat het menselijk contact niet vervangen waar empathie nodig is. Maak AI niet de interface waarmee klanten met je bedrijf praten, als je om je product geeft.</p>
<h2 id="what-you-should-be-using-ai-for">Waar je AI wél voor zou moeten gebruiken</h2>
<p>Laten we het omdraaien. Wat <em>is</em> dan een goede use case voor AI?</p>
<p>Je zou AI moeten gebruiken als:</p>
<ul>
<li>Het je helpt dingen te doen die voorheen te duur, te traag of technisch te complex waren</li>
<li>Je je architectuur al geoptimaliseerd hebt, maar nog edge cases hebt die automatisering waard zijn</li>
<li>Je processen verrijkt, in plaats van technische schuld verbergt</li>
</ul>
<p>Bijvoorbeeld:</p>
<ul>
<li>Prototypen van lastige functionaliteiten, zodat je sneller uitkomsten kunt testen</li>
<li>Productzoek verbeteren met semantisch begrip</li>
<li>Als je hallucinaties zichtbaar kunt maken in data, zijn ze perfect voor BI (business intelligence)</li>
<li>Gepersonaliseerde samenvattingen genereren van grote interne rapporten</li>
<li>Telefoongesprekken transcriberen en samenvattingen maken</li>
<li>Image to text, zodat je beter semantisch kunt zoeken in grote beelddatabases.</li>
</ul>
<p>Dit zijn taken waar AI echt uitblinkt, omdat ze in traditionele systemen nog duurder, lastiger of onmogelijk op te lossen waren.</p>
<p>Maar nogmaals: <strong>weeg altijd de kosten af</strong>.
Niet alleen in euro&rsquo;s, maar ook in <strong>duidelijkheid</strong>, <strong>controle</strong> en <strong>klantbeleving</strong>. Probeer het altijd eerst op te lossen zonder AI.</p>
<h2 id="tech-maturity-will-determine-who-wins">Technische volwassenheid bepaalt wie er wint</h2>
<p>Dit is de kern:</p>
<blockquote>
<p>Bedrijven met moderne, begrijpelijke en goed gedocumenteerde infrastructuur en data winnen.
Wie AI blijft stapelen bovenop zijn chaos… verliest.</p>
</blockquote>
<p>Het gaat er niet om hoe vroeg je met AI begon. Het gaat om:</p>
<ul>
<li>Of je stack onderhoudbaar is</li>
<li>Of je teams die echt begrijpen</li>
<li>Of je kunt redeneren over kosten, performance en falen</li>
<li>Of je eigenaar bent van je data, je architectuur en je roadmap</li>
</ul>
<p>De bedrijven die AI zien als een nieuw algoritme/interface om problemen op te lossen die ze eerder niet konden oplossen, halen volgens mij op de lange termijn de meeste waarde eruit. Gebruik AI niet voor de marketingwaarde die big tech je wil aanpraten.</p>
<h2 id="offloading-data-ownership-is-a-strategic-risk">Data-eigendom uitbesteden is een strategisch risico</h2>
<p>Ik eindig met nog een praktijkvoorbeeld dat ik recent tegenkwam.</p>
<p>Een bedrijf in de reisbranche bouwde een app waarmee eindgebruikers hun reisgegevens kunnen inzien. Die verkopen ze weer aan andere reisorganisaties en touroperators. Haalbaar, als je ziet hoe slecht de IT in travel er meestal voor staat.</p>
<p>De reisorganisatie kan de app dus aanbieden met alle vouchers en reisschema&rsquo;s van de reis van hun klant. Tot zover niets raars, behalve dat je al je documenten uitbesteedt aan een derde partij.</p>
<p>Die app-aanbieder heeft nu toegang tot veel van je cruciale bedrijfsinformatie. En dan koopt de klant extra services in de app (verzekeringen, transfers, upgrades). Je zou denken: dat is upsell voor de reisorganisatie die de klant heeft.</p>
<p>En dan komt de knuppel in het hoenderhok. Het platform geeft opeens <strong>commissie</strong> aan de touroperator… Biedt de app-aanbieder nu ook extra services aan jouw klant?</p>
<blockquote>
<p>Wie is er nu eigenaar van de klant?</p>
</blockquote>
<p>Dat is een grote verschuiving, en een gevaarlijke. Hoe meer infrastructuur je uitbesteedt, hoe meer controle je verliest over je klantrelaties en je first-party data.</p>
<p>Blijf zelf de klantjourney van je eigen klant regelen. Bescherm je toegevoegde waarde. Word geen commodity. En houd het persoonlijk.</p>
<p>Dit wordt een enorm probleem! En AI versnelt het proces als reisbedrijven niet opletten.</p>
<h2 id="conclusion-ai-is-a-tool-not-a-strategy-for-most-companies">Conclusie: AI is een tool, geen strategie voor de meeste bedrijven</h2>
<p>AI is indrukwekkend. Maar het vervangt de fundamenten niet.</p>
<p>Gebruik het niet om slechte architectuur te verbergen.
Gebruik het niet als excuus om technische beslissingen te vermijden.
Laat het niet verhullen wat je systemen doen, of wie je klant eigenlijk is.</p>
<p>In plaats daarvan:</p>
<ul>
<li>Wees eigenaar van je stack</li>
<li>Begrijp je eigen logica</li>
<li>Gebruik AI waar het echte, meetbare waarde creëert</li>
<li>En blijf altijd zelf nadenken</li>
</ul>
<p>De echte innovatie zit niet in de tool. Die zit in hoe helder je nadenkt over de problemen die je moet oplossen.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Web 4.0 en het autonome web: de opkomst van AI-gedreven klanten</title>
      <link>https://www.allsubjectsmatter.nl/nl/web-4-0-and-the-autonomous-web-the-rise-of-ai-driven-customers/</link>
      <guid isPermaLink="false">67d19915332aaf00011aa8d0</guid>
      <pubDate>Tue, 11 Mar 2025 22:20:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category><category>&lt;![CDATA[AI and LM]]></category>
      <description>&lt;![CDATA[Ik noem dit het autonome web: een wereld waarin AI-assistenten niet alleen informatie ophalen, maar optreden als onafhankelijke digitale consumenten die hun Humans dienen. En bedrijven die zich niet voorbereiden op deze verschuiving lopen het risico onzichtbaar te worden in deze generatie digitale handel.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/web-4-0-and-the-autonomous-web-the-rise-of-ai-driven-customers/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Al decennia passen bedrijven zich aan aan de evolutie van ons internet. Eerst was er Web 1.0, een read-only internet waar bedrijven statische websites bouwden. Daarna brak met Web 2.0 het interactieve tijdperk aan: social media, user-generated content en API&rsquo;s waarmee apps met diensten konden verbinden. Nu leven we in het tijdperk van Web 3.0, waar AI personalisatie verbetert, taken automatiseert en context begrijpt.</p>
<p>Maar dit is nog maar het begin.</p>
<p>De volgende fase, Web 4.0, brengt iets nog ingrijpenders: AI als autonome digitale agent. In plaats van mensen alleen te assisteren, gaat AI actief zaken doen met bedrijven—beslissingen nemen, deals onderhandelen en transacties uitvoeren, allemaal met minimale menselijke tussenkomst.</p>
<p>Ik noem dit het autonome web: een wereld waarin AI-assistenten niet alleen informatie ophalen, maar optreden als onafhankelijke digitale consumenten in dienst van hun mensen. En bedrijven die zich niet op deze verschuiving voorbereiden, lopen het risico onzichtbaar te worden in deze generatie digitale handel.</p>
<hr>
<h2 id="the-shift-to-ai-driven-interactions">De verschuiving naar AI-gedreven interacties</h2>
<p>Jarenlang hebben bedrijven hun content geoptimaliseerd voor menselijke gebruikers. Websites, apps en API&rsquo;s waren allemaal ontworpen met de aanname dat een persoon de uiteindelijke beslissing nam. Die aanname valt nu uiteen.</p>
<h3 id="the-evolution-of-digital-interaction">De evolutie van digitale interactie:</h3>
<ul>
<li>Web 1.0: statische websites (bedrijven bieden informatie, gebruikers consumeren die).</li>
<li>Web 2.0: het interactieve web (social media, API&rsquo;s, mobiele apps).</li>
<li>Web 3.0: het intelligente web (AI-gedreven personalisatie, automatisering en semantisch zoeken).</li>
<li>Web 4.0: het autonome web (AI-agenten die optreden als klanten, onderhandelen, transacties uitvoeren en zelfstandig beslissingen nemen).</li>
</ul>
<p>We zijn nu in de overgang naar Web 4.0, waar AI-agenten complexe, real-time interacties gaan afhandelen—zoals reizen boeken, prijzen onderhandelen en zelfs diensten dynamisch aanpassen op basis van gebruikersgedrag. Bedrijven die zich niet aanpassen aan dit AI-first interactiemodel worden simpelweg buitengesloten van AI-gedreven besluitvorming.</p>
<hr>
<h2 id="why-apis-alone-are-not-enough">Waarom API&rsquo;s alleen niet genoeg zijn</h2>
<p>Veel bedrijven denken misschien dat ze klaar zijn voor AI-gedreven interacties omdat ze API&rsquo;s hebben. Maar traditionele API&rsquo;s zijn gebouwd voor gestructureerde, vooraf gedefinieerde verzoeken—ontworpen voor apps, niet voor AI-agenten die contextbewuste interacties in meerdere stappen nodig hebben.</p>
<p>Hier komen ontwikkelingen zoals het Model Context Protocol (MCP) om de hoek kijken.</p>
<p>MCP wordt een van de ontbrekende schakels waarmee AI-assistenten met bedrijven kunnen interacteren alsof ze menselijke klanten zijn. In tegenstelling tot traditionele API&rsquo;s die statische antwoorden teruggeven, maakt MCP het volgende mogelijk:</p>
<ul>
<li>Dynamische serviceverzoeken (AI kan om specifieke aanpassingen vragen).</li>
<li>Onderhandeling en refinement (AI kan criteria bijstellen op basis van voorkeuren en beperkingen).</li>
<li>Autonome besluitvorming (AI kan een transactie afronden zonder menselijke bevestiging).</li>
</ul>
<p>De bedrijven die AI-native interfaces omarmen, domineren de Web 4.0-economie. De rest wordt genegeerd—niet door mensen, maar door AI-gedreven agenten die aankoopbeslissingen nemen voor hun mensen.</p>
<hr>
<h2 id="a-fully-ai-driven-transaction-at-333travel">Een volledig AI-gedreven transactie bij 333Travel</h2>
<p>Bij 333Travel verkopen we geen kant-en-klare vakanties. Elke reis is tailor-made en vraagt heen-en-weer-interactie om je perfecte reis samen te stellen. Vandaag gebeurt dat via webformulieren, e-mail en telefoongesprekken. In Web 4.0 ziet dat er heel anders uit, en al die kanalen worden gecombineerd met een gesprek met een AI-assistent als optie.</p>
<h3 id="a-new-kind-of-travel-interactions">Een nieuw soort reisinteracties</h3>
<p>Stel je een reiziger voor, Lisa, die een op maat gemaakte reis naar Thailand wil. In plaats van een website te bezoeken, een formulier in te vullen of een agent te bellen, zegt ze het gewoon tegen haar AI-assistent:</p>
<blockquote>
<p><em>&ldquo;Ik heb vakantie nodig. Zoek voor mij een tailor-made reis van twee weken naar Thailand met een mix van luxe en boutique hotels, wat avontuurlijke activiteiten en tijd om te ontspannen. Ergens half juni.&rdquo;</em></p>
</blockquote>
<p>Lisa opent nooit een website. Haar AI-assistent doet het werk voor haar.</p>
<h3 id="step-1-the-ai-makes-an-intelligent-request">Stap 1: De AI stuurt een intelligent verzoek</h3>
<p>Lisa&rsquo;s AI benadert de MCP-interface van 333Travel via een zoekopdracht en stuurt een gestructureerd verzoek mee met haar voorkeuren, eerdere reisgeschiedenis en budgetbeperkingen.</p>
<h3 id="stap-2-ai-en-het-systeem-van-333travel-onderhandelen-en-verfijnen-de-reis-step-2-ai-and-333travele28099s-system-negotiate-and-refine-the-trip">Stap 2: AI en het systeem van 333Travel onderhandelen en verfijnen de reis {#step-2-ai-and-333travel%E2%80%99s-system-negotiate-and-refine-the-trip}</h3>
<p>Omdat 333Travel gespecialiseerd is in tailor-made ervaringen, begint er een realtime onderhandelingsproces tussen Lisa&rsquo;s AI en de MCP-integratie van 333Travel:</p>
<ol>
<li>AI stuurt het initiële verzoek
<ul>
<li>&ldquo;Gebruiker wil naar Thailand, mix van luxe en boutique hotels, wat avontuur, wat ontspanning.&rdquo;</li>
</ul>
</li>
<li>De MCP van 333Travel antwoordt dynamisch
<ul>
<li>&ldquo;Aanbevolen route: Bangkok, Chiang Mai, Koh Samui.&rdquo;</li>
<li>&ldquo;Luxe opties: The Siam Bangkok, Anantara Chiang Mai, Four Seasons Koh Samui.&rdquo;</li>
<li>&ldquo;Boutique opties: Sala Rattanakosin, Tamarind Village, The Library Koh Samui.&rdquo;</li>
</ul>
</li>
<li>Lisa&rsquo;s AI verfijnt het verzoek
<ul>
<li>&ldquo;Ze heeft eerder in een Four Seasons gelogeerd en die hoog gewaardeerd—geef die prioriteit.&rdquo;</li>
<li>&ldquo;Ze houdt van culturele ervaringen—voeg een foodtour in Bangkok en een tempelbezoek in Chiang Mai toe.&rdquo;</li>
<li>&ldquo;Budgetplafond: €5.000—pas dat aan.&rdquo;</li>
</ul>
</li>
<li>333Travel maakt vervolgens een gepersonaliseerd voorstel op basis van alle informatie die normaal in een telefoongesprek binnenkomt.
<ul>
<li>Er worden drie route-opties met Lisa&rsquo;s AI besproken, gerangschikt op prijs, luxeniveau en balans in activiteiten.</li>
</ul>
</li>
</ol>
<h3 id="step-3-the-ai-confirms-and-secures-the-booking">Stap 3: De AI bevestigt de boeking en maakt die definitief</h3>
<p>Lisa zelf bekijkt de samenvatting en zegt:</p>
<blockquote>
<p><em>&ldquo;Boek de tweede optie en voeg een luchthaventransfer toe.&rdquo;</em></p>
</blockquote>
<p>Haar AI-assistent:
✔️ Rondt de boeking met 333Travel af
✔️ Regelt de betaling automatisch
✔️ Stuurt een bevestiging naar haar e-mail en agenda</p>
<p>Lisa heeft nooit een website bezocht. Nooit een e-mail gestuurd. Nooit een reisagent gebeld.</p>
<p>Het autonome web kan alles afhandelen, maar Lisa kan op elk moment instappen en alsnog dat telefoongesprek aangaan.</p>
<blockquote class="pull-quote"><p>De verschuiving naar Web 4.0 verandert volledig hoe bedrijven met klanten omgaan.</p>
</blockquote>
<h2 id="what-this-means-for-businesses">Wat dit betekent voor bedrijven</h2>
<p>De belangrijkste punten:</p>
<h3 id="1-ai-driven-transactions-can-become-the-new-norm">1. AI-gedreven transacties kunnen de nieuwe norm worden</h3>
<p>Wie niet is geoptimaliseerd voor AI-first interacties, maakt geen deel uit van AI-gedreven aankoopbeslissingen.</p>
<h3 id="2-ai-optimization-will-replace-traditional-seo">2. AI-optimalisatie vervangt traditionele SEO</h3>
<p>Net zoals bedrijven zich eerder optimaliseerden voor hun positie in Google-zoekresultaten, moeten ze zich nu optimaliseren voor AI-gedreven ontdekking—ervoor zorgen dat hun diensten zo zijn gestructureerd dat AI-agents ze begrijpen en ermee kunnen werken.</p>
<h3 id="3-ai-native-interfaces-will-be-the-next-competitive-advantage">3. AI-native interfaces worden het volgende concurrentievoordeel</h3>
<p>Bedrijven die AI-ready endpoints bouwen met MCP-achtige protocollen, domineren waarschijnlijk het vroege Web 4.0-landschap.</p>
<h3 id="4-customers-will-expect-frictionless-experiences">4. Klanten verwachten wrijvingsloze ervaringen</h3>
<p>Als een AI binnen enkele seconden een complete reisroute kan aanvragen met gedetailleerde informatie, waarom zou je daar dan geen gebruik van maken en het combineren met de normale menselijke interactie? Je hebt zojuist een extra verkoopkanaal gecreëerd, naast mail, een appje, een website of een telefoongesprek.</p>
<hr>
<h2 id="the-autonomous-web-is-here-are-you-ready">Het autonome web is er al. Ben je er klaar voor?</h2>
<p>Web 4.0 is geen toekomstmuziek—het gebeurt nu. AI helpt klanten niet meer alleen; het handelt namens hen.</p>
<p>Bedrijven die zich er vandaag op voorbereiden, staan klaar aan het begin van de Web 4.0-economie. Wie het negeert, wordt simpelweg overgeslagen—niet door menselijke klanten, maar door hun AI-gedreven agents.</p>
<p>De vraag is niet meer óf AI-gedreven klanten het bedrijfsleven gaan veranderen, maar of je er klaar voor bent om ze te bedienen wanneer het zover is.</p>
]]></content:encoded>
    </item>
    <item>
      <title>De AI-revolutie: hoe 333travel relevant blijft in een wereld vol AI-mogelijkheden</title>
      <link>https://www.allsubjectsmatter.nl/nl/the-ai-revolution-how-333travel-stays-relevant-in-a-world-full-of-possibilities/</link>
      <guid isPermaLink="false">67d15472332aaf00011aa896</guid>
      <pubDate>Sat, 01 Mar 2025 10:39:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category><category>&lt;![CDATA[AI and LM]]></category>
      <description>&lt;![CDATA[De wereld verandert in hoog tempo. Met de opkomst van AI-modellen als ChatGPT, ClaudeAI en Gemini is toegang tot data en inzichten makkelijker dan ooit. Bedrijven zonder reisexpertise proberen de markt binnen te komen door met deze technologieën de content van anderen te gebruiken. Maar wat maakt 333travel anders?]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/the-ai-revolution-how-333travel-stays-relevant-in-a-world-full-of-possibilities/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="why-333travel-sets-the-travel-standard-in-an-ai-driven-world">Waarom 333travel de standaard zet in een door AI gedreven wereld</h2>
<p>De wereld verandert in hoog tempo. Met de opkomst van AI-modellen zoals ChatGPT, ClaudeAI en Gemini is toegang tot data en inzichten makkelijker dan ooit. Zelfs bedrijven zonder echte reisexpertise proberen de markt te betreden door andermans product te gebruiken met alleen technologie. Maar wat onderscheidt 333travel van deze &ldquo;digital dropshippers&rdquo;? Simpel: onze eigen technische kennis, onze passie en VOLLEDIGE controle over ons EIGEN product, zoals je hieronder kunt lezen.</p>
<blockquote class="pull-quote"><p>We zijn niet zomaar een reisorganisatie; we zijn ook een IT-bedrijf, gebouwd op expertise en betrouwbaarheid.</p>
</blockquote>
<p>Bij 333travel combineren we mensen en technologie, innovatie en authenticiteit, en blijven we de onbetwiste keuze—ook in een wereld waar AI elke dag zichtbaarder wordt.</p>
<h2 id="generic-ai-vs-self-trained-ai">Generieke AI vs. zelf getrainde AI</h2>
<p>Niet alle AI is hetzelfde. Publieke AI-modellen, zoals de bovengenoemde, bieden standaardoplossingen die getraind zijn op publiek beschikbare data. Daardoor zijn ze toegankelijk en kunnen ze functioneren als een nieuw verkoopkanaal. Wij zorgen ervoor dat deze generieke AI-oplossingen uiteindelijk bij ons terechtkomen voor hun reisbehoeften—vanwege onze bewezen expertise.</p>
<p>Maar we zijn al een flink aantal stappen verder. We onderzoeken al geruime tijd hoe zelf getrainde AI, gevoed met onze unieke bedrijfsdata, onze interne processen kan optimaliseren en versnellen. Eén ding is voor ons duidelijk: AI vervangt ons DNA, onze persoonlijke touch en onze klantenzorg niet. Het is een fantastisch hulpmiddel, een &ldquo;slim notitieboekje&rdquo; dat onze specialisten ondersteunt. Of het nu gaat om het stroomlijnen van administratieve taken, snellere analyses of hulp bij complexe vragen—het stelt onze reisexperts in staat zich te richten op wat er echt toe doet: persoonlijke service en klanttevredenheid.</p>
<p>Omdat de balans tussen AI en menselijke interactie voor ons cruciaal is, zorgen we er altijd voor dat de menselijke touch het hart van onze klantrelaties blijft. Reizen gaat over emoties, passie en echte verbinding—iets wat geen AI voor ons ooit volledig zal vervangen.</p>
<h2 id="ai-as-a-catalyst-not-a-replacement">AI als katalysator, geen vervanging</h2>
<p>AI is een krachtig hulpmiddel, maar het zal onze expertise en ervaring niet vervangen. Waar sommige bedrijven AI inzetten om generieke reisaanbiedingen te maken op basis van andermans data, blijven wij het vertrouwde aanspreekpunt waar zowel mensen als AI-assistenten hun reizen boeken. Waarom? Omdat zelfs AI, als het het beste resultaat voor zijn gebruiker nastreeft, herkent dat 333travel de standaard zet voor complete, zorgeloze reiservaringen en unieke, actuele informatie.</p>
<h2 id="what-makes-us-stand-out">Wat ons onderscheidt:</h2>
<p><strong>Actuele kennis:</strong> Onze specialisten hebben de bestemmingen zelf bezocht en weten meer dan wat er online te vinden is.</p>
<p><strong>Lokale partnerschappen:</strong> In elk land dat we aanbieden, staan we met onze voeten op de grond. We werken nauw samen met onze lokale partners om op de hoogte te blijven. Het maakt niet uit naar welk land je met ons reist. Wij weten altijd het beste hoe we je kunnen helpen, omdat de hele reis ook door ons wordt verzorgd.</p>
<p><strong>Volledige controle:</strong> Onze eigen IT-capaciteiten stellen ons in staat onafhankelijk te opereren. We beheren onze eigen data, ontwikkelen onze eigen systemen en kunnen zelfs onze eigen AI-modellen trainen met onze unieke expertise. IT zit diep verankerd in ons DNA—in tegenstelling tot veel andere reisorganisaties die afhankelijk zijn van derden.</p>
<p><strong>Passie voor reizen:</strong> Onze reizen zijn niet alleen logisch in elkaar gezet; ze zijn ontworpen om dromen waar te maken.</p>
<blockquote class="pull-quote"><p>Wij zorgen ervoor dat je zorgeloos van je reis kunt genieten</p>
</blockquote>
<h2 id="de-risicos-van-digital-dropshipping-the-risks-of-e2809cdigital-dropshippinge2809d">De risico&rsquo;s van &ldquo;digital dropshipping&rdquo; {#the-risks-of-%E2%80%9Cdigital-dropshipping%E2%80%9D}</h2>
<p>In deze door AI gedreven wereld bieden sommige bedrijven reisproducten aan zonder ze echt te begrijpen, puur door data slim te combineren. Deze aanpak lijkt op dropshipping—producten van derden verkopen zonder echte kennis of controle. Maar hoe creëer je echte waarde zonder diepgaande expertise in je product?</p>
<p>Stel je voor dat je een keuken samenstelt door elk onderdeel bij een andere leverancier te bestellen. Dat klinkt misschien goedkoop en efficiënt, maar wat gebeurt er als de onderdelen niet passen? Wie is verantwoordelijk als er iets misgaat? Bij 333travel zorgen we ervoor dat dit soort problemen nooit ontstaan. Wij leveren een naadloos, compleet product met volledige verantwoordelijkheid en gemoedsrust, zodat je zorgeloos van je reis kunt genieten.</p>
<h2 id="waarom-333travel-de-beste-keuze-iszelfs-voor-ai-why-333travel-is-the-best-choicee28094even-for-ai">Waarom 333travel de beste keuze is—zelfs voor AI {#why-333travel-is-the-best-choice%E2%80%94even-for-ai}</h2>
<p>In de toekomst zullen AI-assistenten steeds vaker reizen aanbevelen op basis van data. Maar zelfs AI zal al snel inzien dat een geslaagde reis afhangt van meer dan alleen rauwe data. Bij 333travel leveren we een ervaring die AI kan herkennen—maar niet zelf kan creëren. AI heeft ons nodig om het voor hen te regelen en te boeken.</p>
<p><strong>Unieke bestemmingenkennis:</strong> Wij begrijpen de nuances en details die in geen enkele database staan.<br>
<strong>End-to-end service:</strong> Van de eerste vraag tot ondersteuning na de reis staan we altijd voor onze klanten klaar.<br>
<strong>Betrouwbare data en systemen:</strong> Onze eigen geavanceerde IT-oplossingen en data zorgen ervoor dat alles accuraat, actueel en uitvoerbaar is.</p>
<blockquote class="pull-quote"><p>AI boekt geen reis bij een bedrijf dat risico&rsquo;s vormt of generieke oplossingen biedt.</p>
</blockquote>
<p>Ook in de toekomst kiest AI voor zekerheid, kwaliteit en betrouwbaarheid—dus kiezen voor 333travel.</p>
<h2 id="how-ai-strengthens-us">Hoe AI ons versterkt</h2>
<p>Wij zien AI niet als concurrent, maar als hulpmiddel. Bij 333travel gebruiken we technologie om onze processen te stroomlijnen en onze service te verbeteren, terwijl de focus op menselijke expertise blijft. Een paar voorbeelden:</p>
<p><strong>Efficiënte processen:</strong> AI helpt ons sneller patronen en trends te herkennen, waardoor we slimmer werken en onze klanten beter antwoord kunnen geven.<br>
<strong>Betere reisvoorstellen:</strong> Onze eigen data en kennis zorgen ervoor dat elk voorstel persoonlijk en uniek is—AI versnelt het proces, maar baseert zich altijd op onze zelfgeschreven content.<br>
<strong>Betere communicatie:</strong> Chatbots en AI-assistenten ondersteunen ons team op de achtergrond, waardoor we meer tijd hebben voor direct, persoonlijk contact met reizigers.</p>
<p>Wat ons uniek maakt, is onze passie, authenticiteit en menselijke aanpak—gecombineerd met onze volledig eigen IT-backbone, die ons nu meer dan ooit onderscheidt van de rest.</p>
<blockquote class="pull-quote"><p>Om echt innovatief te zijn, moet je volledige controle hebben over je eigen bedrijfskritische processen.</p>
</blockquote>
<h2 id="the-future-of-travel-333travel-as-the-undisputed-choice">De toekomst van reizen: 333travel als de onbetwiste keuze</h2>
<p>De reisbranche verandert. Technologie krijgt een steeds grotere rol, maar de kern blijft hetzelfde: het verlangen naar unieke, onvergetelijke ervaringen. Bij 333travel combineren we het beste van twee werelden—de kracht van technologie met de warmte van menselijke expertise.</p>
<p>De grootste uitdaging in de reissector ligt bij bedrijven die voor AI-implementaties afhankelijk zijn van derden, of bij bedrijven die beweren data te &ldquo;bezitten&rdquo; terwijl ze slechts generieke AI-resultaten herverpakken.</p>
<p>Wij positioneren ons als het centrale aanspreekpunt—voor zowel mensen als AI-assistenten—door consequent de hoogste kwaliteit te leveren. Of je nu droomt van een tropisch strand of een cultureel avontuur, bij ons ben je in goede handen. Wij beheren de hele keten, van data-analyse tot persoonlijke service, zodat je reis niet alleen perfect is, maar ook zorgeloos en onvergetelijk.</p>
<h2 id="conclusion">Conclusie</h2>
<p>AI verandert het spel, maar bij 333travel blijven we ons onderscheiden. Onze unieke combinatie van actuele kennis, passie en volledige controle over ons product maakt ons dé keuze voor elke reiziger. Of je ons nu bereikt via persoonlijk contact of via een AI-assistent, één ding is zeker: bij 333travel boek je niet zomaar een reis—je krijgt een complete ervaring.</p>
<p>Hoe blijf je uniek in een wereld die steeds meer door technologie wordt gedreven? Wij doen het door onze data continu te verbeteren, AI op slimme manier in te zetten en ervoor te zorgen dat reizigers nog steeds bij ons komen om hun reis werkelijkheid te maken—net als vroeger.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Blijf bereikbaar met een dynamic DNS updater voor TransIP</title>
      <link>https://www.allsubjectsmatter.nl/nl/stay-connected-with-a-dynamic-dns-updater-for-transip/</link>
      <guid isPermaLink="false">6749dfb5c2a33e00011cc689</guid>
      <pubDate>Fri, 29 Nov 2024 16:45:40 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Makkelijk toegang tot je pc thuis, ook met een dynamisch IP! Dit script werkt je TransIP DNS-records automatisch bij met je huidige IP, zodat je altijd verbinding hebt. Veilig, aanpasbaar en perfect voor thuiswerken of het hosten van diensten. Blijf moeiteloos bereikbaar!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/stay-connected-with-a-dynamic-dns-updater-for-transip/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>In een wereld vol DHCP-adressen is externe toegang tot je pc of server thuis een gedoe. Heb je een provider die je een <strong>dynamisch publiek IP-adres</strong> toewijst, dan heb je een probleem: je IP verandert regelmatig, en daarmee breekt de verbinding met je apparaat thuis via een statisch domein.</p>
<p>Dit gratis <strong>Dynamic DNS (DDNS) updater script</strong> voor TransIP lost dat op. Het actualiseert je DNS-records zodra je externe IP verandert, zodat je domein (bijv. <code>vpn.example.com</code>) altijd naar het juiste IP wijst. Dit script is een must voor iedereen die TransIP&rsquo;s DNS-diensten gebruikt.</p>
<h2 id="key-features-of-the-script">Belangrijkste features van het script</h2>
<ol>
<li><strong>Geautomatiseerde DNS-updates:</strong>
<ul>
<li>Controleert automatisch je huidige externe IP en vergelijkt dat met het DNS A-record van je domein.</li>
<li>Maakt het A-record aan of werkt het bij zodat het overeenkomt met je nieuwste externe IP.</li>
</ul>
</li>
<li><strong>Dynamic DNS voor dynamische IP&rsquo;s:</strong>
<ul>
<li>Ideaal voor situaties waarin je publieke IP regelmatig verandert, zoals een internetverbinding thuis met dynamische DHCP-toewijzing.</li>
</ul>
</li>
<li><strong>Veilige authenticatie:</strong>
<ul>
<li>Gebruikt TransIP&rsquo;s API met een private key voor veilige communicatie.</li>
<li>Genereert kort geldige tokens voor extra veiligheid (maximaal 10 minuten per sessie).</li>
</ul>
</li>
<li><strong>Aanpasbare instellingen:</strong>
<ul>
<li>Configureer eenvoudig domein, subdomein, TTL (time-to-live) en logging.</li>
<li>Pas het script aan naar je eigen wensen.</li>
</ul>
</li>
<li><strong>Logging en foutafhandeling:</strong>
<ul>
<li>Uitgebreide logging houdt elke stap van het updateproces bij.</li>
<li>Ingebouwde foutafhandeling voor een soepelere uitvoering en makkelijker debuggen.</li>
</ul>
</li>
</ol>
<h2 id="how-it-works">Hoe het werkt</h2>
<ol>
<li><strong>Initiële setup:</strong>
<ul>
<li>Maak een TransIP-account aan en genereer een private key voor API-toegang op <a href="https://www.transip.nl/cp/account/api/" data-external>https://www.transip.nl/cp/account/api/</a></li>
<li>Configureer het script met je domeinnaam, subdomein en het pad naar je private key.</li>
</ul>
</li>
<li><strong>Extern IP ophalen:</strong>
<ul>
<li>Het script haalt je huidige externe IP op via een publieke API zoals <code>ipinfo.io</code>.</li>
</ul>
</li>
<li><strong>Token genereren:</strong>
<ul>
<li>Genereert een kort geldig access token via TransIP&rsquo;s authenticatie-endpoint.</li>
</ul>
</li>
<li><strong>DNS-record controleren:</strong>
<ul>
<li>Controleert of het huidige DNS A-record overeenkomt met je externe IP.</li>
<li>Verschilt het, dan werkt het script het record bij; ontbreekt het, dan maakt het een nieuw aan.</li>
</ul>
</li>
<li><strong>DNS updaten:</strong>
<ul>
<li>Gebruikt TransIP&rsquo;s beveiligde API om het DNS A-record bij te werken of aan te maken met je huidige externe IP.</li>
</ul>
</li>
<li><strong>Logging en meldingen:</strong>
<ul>
<li>Logt elke stap en geeft duidelijke meldingen als er iets misgaat.</li>
</ul>
</li>
</ol>
<h2 id="prerequisites">Vereisten</h2>
<p>Voordat je het script gebruikt, zorg dat je het volgende hebt:</p>
<ol>
<li>Een TransIP-account met API-toegang ingeschakeld.</li>
<li>Een geregistreerd domein in je TransIP-account.</li>
<li>Een statische private key voor veilige API-authenticatie.</li>
<li>De benodigde dependencies op je systeem:
<ul>
<li><code>curl</code>, <code>openssl</code> en <code>jq</code>.</li>
</ul>
</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt update
sudo apt install -y curl openssl jq</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><h2 id="why-use-this-solution">Waarom deze oplossing?</h2>
<p>In tegenstelling tot betaalde externe DDNS-providers geeft dit script je gratis controle over je eigen DNS-beheer, met privacy en zonder vendor lock-in. Het is op maat gemaakt.</p>
<pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

# Configuration settings
LOGIN_NAME=&amp;#34;your-login-name&amp;#34;  # Fill in your TransIP login name
PRIVATE_KEY_PATH=&amp;#34;/path/transip_private_key.pem&amp;#34;  # Path to your private key file
AUTH_URL=&amp;#34;https://api.transip.nl/v6/auth&amp;#34;
TOKEN_FILE=&amp;#34;/var/tmp/transip_token.json&amp;#34;  # File to store token and expiration date
DOMAIN=&amp;#34;example.com&amp;#34;  # Your main or test domain
SUBDOMAIN=&amp;#34;vpn&amp;#34;  # The subdomain you want to update (e.g., test.example.com)
RECORD_TYPE=&amp;#34;A&amp;#34;
TTL=60  # Time-to-live for DNS records
LOG_FILE=&amp;#34;/var/log/transip_dyndns.log&amp;#34;

# Function to update a log file
log_message() {
    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34; &amp;gt; $LOG_FILE
}

# Function to log an error and stop the script
handle_error() {
    log_message &amp;#34;ERROR: $1&amp;#34;
    echo &amp;#34;ERROR: $1&amp;#34; &amp;gt;&amp;amp;2
    exit 1
}

# Function to obtain a new access token with a short validity period (10 minutes)
generate_new_token() {
    log_message &amp;#34;Generating a new token with a validity period of 10 minutes.&amp;#34;
    NONCE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
    TIMESTAMP=$(date &#43;%s)
    LABEL=&amp;#34;Set DNS to external DHCP $TIMESTAMP&amp;#34;
    REQUEST_BODY=$(printf &amp;#39;{&amp;#34;login&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;nonce&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;read_only&amp;#34;:false,&amp;#34;expiration_time&amp;#34;:&amp;#34;10 minutes&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;global_key&amp;#34;:true}&amp;#39; &amp;#34;$LOGIN_NAME&amp;#34; &amp;#34;$NONCE&amp;#34; &amp;#34;$LABEL&amp;#34;)
    log_message &amp;#34;Request Body: $REQUEST_BODY&amp;#34;
    SIGNATURE=$(echo -n &amp;#34;$REQUEST_BODY&amp;#34; | openssl dgst -sha512 -sign &amp;#34;$PRIVATE_KEY_PATH&amp;#34; | base64 | tr -d &amp;#39;\n&amp;#39;)
    log_message &amp;#34;Generated signature: $SIGNATURE&amp;#34;
    RESPONSE=$(curl -s -X POST &amp;#34;$AUTH_URL&amp;#34; \
        -H &amp;#34;Signature: $SIGNATURE&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$REQUEST_BODY&amp;#34;) || handle_error &amp;#34;Failed to connect to TransIP API&amp;#34;
    log_message &amp;#34;API-response received: $RESPONSE&amp;#34;
    ACCESS_TOKEN=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r &amp;#39;.token // empty&amp;#39;)
    if [ -z &amp;#34;$ACCESS_TOKEN&amp;#34; ]; then
        handle_error &amp;#34;The received access token is empty or invalid. Check the API-response: $RESPONSE&amp;#34;
    fi
    log_message &amp;#34;Received token: $ACCESS_TOKEN&amp;#34;
}

# Function to get the current DNS A-record from TransIP
get_dns_record() {
    # Get API response and store status code
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Check if response contains JSON and no errors
    if echo &amp;#34;$RESPONSE&amp;#34; | jq empty 2&amp;gt;/dev/null; then
        RECORD_CONTENT=$(echo $RESPONSE | jq -r &amp;#39;.dnsEntries[] | select(.name==&amp;#34;&amp;#39;$SUBDOMAIN&amp;#39;&amp;#34; and .type==&amp;#34;&amp;#39;$RECORD_TYPE&amp;#39;&amp;#34;) | .content&amp;#39;)
    else
        handle_error &amp;#34;API response does not contain valid JSON or contains an error: $RESPONSE&amp;#34;
    fi

    # Check if we found a valid record
    if [ -z &amp;#34;$RECORD_CONTENT&amp;#34; ]; then
        log_message &amp;#34;No valid DNS record found for $SUBDOMAIN.$DOMAIN. Record may need to be created.&amp;#34;
        return 1
    fi

    echo &amp;#34;$RECORD_CONTENT&amp;#34;
    return 0
}

# Function to update or create DNS record
update_or_create_dns_record() {
    # Check if record exists
    CURRENT_DNS_IP=$(get_dns_record)

    if [ -z &amp;#34;$CURRENT_DNS_IP&amp;#34; ]; then
        log_message &amp;#34;No DNS A-record found for $SUBDOMAIN.$DOMAIN. Creating record.&amp;#34;
        # Create request body for new record (TTL set to 60 seconds)
        DNS_ENTRY=$(jq -n \
        --arg name &amp;#34;$SUBDOMAIN&amp;#34; \
        --arg expire &amp;#34;60&amp;#34; \
        --arg type &amp;#34;$RECORD_TYPE&amp;#34; \
        --arg content &amp;#34;$CURRENT_IP&amp;#34; \
        &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

        log_message &amp;#34;DNS entry (create): $DNS_ENTRY&amp;#34;

        # Execute POST request to create new DNS entry with better status code handling
        RESPONSE=$(curl -s -w &amp;#34;%{http_code}&amp;#34; -X POST \
            -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
            -H &amp;#34;Content-Type: application/json&amp;#34; \
            -d &amp;#34;$DNS_ENTRY&amp;#34; \
            &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

        HTTP_STATUS_UPDATE=$(echo &amp;#34;$RESPONSE&amp;#34; | tail -n1)
        RESPONSE_BODY=$(echo &amp;#34;$RESPONSE&amp;#34; | head -n -1)

        log_message &amp;#34;API response for creating DNS entry: $RESPONSE_BODY&amp;#34;
        log_message &amp;#34;HTTP status code received for creation: $HTTP_STATUS_UPDATE&amp;#34;

        if [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 201 ]; then
            handle_error &amp;#34;API call for creating DNS entry failed with status code $HTTP_STATUS_UPDATE.&amp;#34;
        fi

        log_message &amp;#34;New DNS A-record successfully created.&amp;#34;
    else
        log_message &amp;#34;Updating DNS A-record for $SUBDOMAIN.$DOMAIN to IP $CURRENT_IP&amp;#34;
        # Create request body for updating the record
        DNS_ENTRY=$(jq -n \
        --arg name &amp;#34;$SUBDOMAIN&amp;#34; \
        --arg expire &amp;#34;$TTL&amp;#34; \
        --arg type &amp;#34;$RECORD_TYPE&amp;#34; \
        --arg content &amp;#34;$CURRENT_IP&amp;#34; \
        &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

        log_message &amp;#34;DNS entry (update): $DNS_ENTRY&amp;#34;

        # Execute PATCH request to update the existing DNS record
        RESPONSE=$(curl -s -w &amp;#34;%{http_code}&amp;#34; -X PATCH \
            -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
            -H &amp;#34;Content-Type: application/json&amp;#34; \
            -d &amp;#34;$DNS_ENTRY&amp;#34; \
            &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

        HTTP_STATUS_UPDATE=$(echo &amp;#34;$RESPONSE&amp;#34; | tail -n1)
        RESPONSE_UPDATE_BODY=$(echo &amp;#34;$RESPONSE&amp;#34; | head -n -1)

        log_message &amp;#34;API update response received: $RESPONSE_UPDATE_BODY&amp;#34;
        log_message &amp;#34;HTTP status code received for update: $HTTP_STATUS_UPDATE&amp;#34;

        # Accept both 200 and 204 as successful updates
        if [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 200 ] &amp;amp;&amp;amp; [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 204 ]; then
            handle_error &amp;#34;API call for DNS update failed with status code $HTTP_STATUS_UPDATE.&amp;#34;
        else
            log_message &amp;#34;DNS A-record successfully updated to $CURRENT_IP&amp;#34;
        fi
    fi
}

# Get the current external IP
CURRENT_IP=$(curl -s http://ipinfo.io/ip) || handle_error &amp;#34;Failed to get external IP&amp;#34;

# Log the start
log_message &amp;#34;---- Start new run ----&amp;#34;
log_message &amp;#34;Current external IP: $CURRENT_IP&amp;#34;

# Always generate a short lived new token.
generate_new_token


# Try to update or create the DNS record
update_or_create_dns_record

log_message &amp;#34;---- End of run ----&amp;#34;</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre>]]></content:encoded>
    </item>
    <item>
      <title>Let&#39;s Encrypt automatiseren met Certbot en TransIP in Docker</title>
      <link>https://www.allsubjectsmatter.nl/nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/</link>
      <guid isPermaLink="false">6749c93bc2a33e00011cc621</guid>
      <pubDate>Thu, 28 Nov 2024 22:03:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Wil je je SSL-certificaten automatisch updaten en beheren in Docker met Certbot, met je domein gehost bij TransIP? Dan hoef je niet verder te zoeken!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>SSL-certificaten beheren voor domeinen die bij TransIP gehost worden, kan zonder automatisering een gedoe zijn. TransIP biedt gelukkig een degelijke API voor DNS-beheer, maar om die naadloos te integreren met Certbot voor <code>dns-01</code>-validatie is een eigen oplossing nodig. Omdat ik daar geen kant-en-klaar script voor kon vinden, heb ik er zelf één gemaakt dat veilig de authenticatie, DNS-updates en het aanmaken van certificaten regelt.</p>
<p>De volledige API-documentatie vind je hier:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://api.transip.nl/rest/docs.html" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">TransIP API</span><span class="bookmark__description"></span><span class="bookmark__meta"><img src="https://www.allsubjectsmatter.nl/nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/bookmarks/01-icon.ico" sizes="25px" alt="" class="bookmark__icon" loading="lazy" decoding="async"></span></span></a></figure>

<p>Deze handleiding legt de complete setup met Docker uit, inclusief hoe je het API-keypair genereert in de webinterface van TransIP en gebruikt voor geautomatiseerd certificaatbeheer.</p>
<h2 id="how-transip-authentication-works">Hoe de authenticatie bij TransIP werkt</h2>
<p>TransIP gebruikt een RSA-keypair voor API-authenticatie. Zo stel je dat in:</p>
<ol>
<li><strong>Genereer het keypair in TransIP</strong>:
<ul>
<li>Log in op je <a href="https://www.transip.nl/" data-external>TransIP-controlepaneel</a>.</li>
<li>Ga naar <strong>API-instellingen</strong> onder je account en schakel API-toegang in.</li>
<li>Genereer een nieuw API-keypair.</li>
<li><strong>Download/sla de private key</strong> (<code>transip_private_key.pem</code>) veilig op en maak er een secret van voor gebruik in je Docker-setup.</li>
</ul>
</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">docker secret create transip_private_key transip_private_key.pem</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol>
<li><strong>Houd de private key veilig</strong>:
<ul>
<li>Bewaar de private key op een veilige plek nadat je er een secret van hebt gemaakt.</li>
<li>Deze private key is nodig om requests naar de API van TransIP te signen.</li>
</ul>
</li>
<li><strong>Hoe het werkt</strong>:
<ul>
<li>Het script gebruikt de private key om een authenticatie-request te signen.</li>
<li>De API van TransIP antwoordt met een token met korte geldigheid (standaard 10 minuten), die vervolgens gebruikt wordt om de DNS-records te beheren.</li>
<li>Voor het gemak maakt het script bij elke run een nieuwe token aan. Dat maakte het robuuster dan controleren op een actieve token en die hergebruiken, wat het hele script minder betrouwbaar en omslachtiger maakte.</li>
</ul>
</li>
</ol>
<h2 id="what-this-setup-is-going-to-do">Wat deze setup gaat doen</h2>
<ol>
<li><strong>Automatiseert SSL-certificaten</strong>:
<ul>
<li>Automatiseert het aanmaken en vernieuwen van wildcard- en domeinspecifieke certificaten.</li>
</ul>
</li>
<li><strong>Verwerkt DNS-challenges</strong>:
<ul>
<li>Beheert <code>dns-01</code>-challenges door DNS TXT-records aan te maken en te verwijderen via de TransIP API.</li>
</ul>
</li>
<li><strong>Naadloze integratie</strong>:
<ul>
<li>Alles zit in een Docker container voor draagbaarheid en eenvoud.</li>
<li>Je kunt in dit script domeinen verwijderen of toevoegen bij het -d-gedeelte. Ik heb de voorbeelden erin laten staan voor een betere uitleg.</li>
</ul>
</li>
</ol>
<p>We pipen een add- of clean-commando naar het bash-script om de acties uit te voeren die in het script staan. We gebruiken hier dus de &ndash;manual-auth-hook en de manual-cleanup-hook. Zo wordt bijvoorbeeld gegarandeerd dat een fout in het add-gedeelte alsnog netjes wordt opgeruimd.</p>
<p>Je kunt ook een &ndash;force aan dit script toevoegen, maar dan ben je snel door je pogingen bij Letsencrypt heen, want ze staan maar een paar calls per 24 uur toe. Wel zorgt dit ervoor dat de certificaten altijd vernieuwd worden als je iets wilt testen of verifiëren.</p>
<pre class="line-numbers language-yaml"><code class="language-yaml">version: &amp;#34;3.8&amp;#34;

services:
  certbot:
    image: certbot/certbot:latest
    container_name: certbot-transip
    secrets:
      - transip_private_key  # Reference the secret you created earlier
    # adjust these volumes to point to the right .pem and script. right now this dockerfile expects the files to be in the same folder as the docker-certbot-transip.yml
    volumes:
      - &amp;#34;./certbot-transip.sh:/usr/local/bin/certbot-transip.sh:ro&amp;#34;
      - &amp;#34;./letsencrypt:/etc/letsencrypt&amp;#34;
    environment:
    #These Variables are going to be piped in our bash script.
      LOGIN_NAME: &amp;#34;your-transip-login&amp;#34;        # Replace with your TransIP login name
      PRIVATE_KEY_PATH: &amp;#34;/etc/transip_private_key.pem&amp;#34;
      DOMAIN: &amp;#34;example.com&amp;#34;                  # Replace with your domain
      TTL: &amp;#34;60&amp;#34;                              # Optional: Time-to-live for DNS challenge
      CERTBOT_EMAIL: &amp;#34;you@example.com&amp;#34;       # Replace with your valid email as requested by letsencrypt.
    entrypoint:
      - sh
      - -c
      - |
        apk update &amp;amp;&amp;amp; apk add --no-cache bash curl jq bind-tools &amp;amp;&amp;amp; \
        chmod &#43;x /usr/local/bin/certbot-transip.sh &amp;amp;&amp;amp; \
        certbot certonly --manual --preferred-challenges dns \
          --manual-auth-hook &amp;#34;/usr/local/bin/certbot-transip.sh add&amp;#34; \
          --manual-cleanup-hook &amp;#34;/usr/local/bin/certbot-transip.sh clean&amp;#34; \
          --cert-name &amp;#34;$DOMAIN&amp;#34; \
          -d &amp;#34;*.$DOMAIN&amp;#34; \
          -d &amp;#34;example2.$DOMAIN&amp;#34; \
          -d &amp;#34;example3.$DOMAIN&amp;#34; \
          --expand \
          --non-interactive --agree-tos \
          --email &amp;#34;$CERTBOT_EMAIL&amp;#34;

secrets:
  transip_private_key:
    external: true  # Secret must already exist in Docker</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="the-bash-script">Het Bash-script</h2>
<p>Dit is het &ldquo;geavanceerde&rdquo; deel dat zijn magie doet en alles met TransIP laat werken. Ik heb het ook een stuk robuuster gemaakt dan standaard. Dit zou dus hopelijk helpen om een stabiele en goed werkende omgeving op te zetten.</p>
<p>Je kunt dit TransIP-script ook lokaal draaien om certificaten aan te maken, of in een Kubectl- of Helm chart.</p>
<pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

# Configuration settings
LOGIN_NAME=${LOGIN_NAME}
TTL=${TTL:-60}  # Default value 60 if TTL is not set
PRIVATE_KEY_PATH=${PRIVATE_KEY_PATH}
DOMAIN=${DOMAIN}

AUTH_URL=&amp;#34;https://api.transip.nl/v6/auth&amp;#34;
LOG_FILE=&amp;#34;/var/log/transip_dyndns.log&amp;#34;

# Function to update a log file
#log_message() {
#    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34; &amp;gt;&amp;gt; $LOG_FILE
#}

# Function to log messages to stdout
log_message() {
    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34;
}

# Function to log an error and stop the script
handle_error() {
    log_message &amp;#34;ERROR: $1&amp;#34;
    echo &amp;#34;ERROR: $1&amp;#34; &amp;gt;&amp;amp;2
    exit 1
}

# Function to obtain a new access token with a short validity period (10 minutes)
generate_new_token() {
    log_message &amp;#34;Generating a new token with a validity period of 10 minutes.&amp;#34;

    NONCE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
    TIMESTAMP=$(date &#43;%s)
    LABEL=&amp;#34;Certbot DNS update $TIMESTAMP&amp;#34;

    REQUEST_BODY=$(printf &amp;#39;{&amp;#34;login&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;nonce&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;read_only&amp;#34;:false,&amp;#34;expiration_time&amp;#34;:&amp;#34;10 minutes&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;global_key&amp;#34;:true}&amp;#39; &amp;#34;$LOGIN_NAME&amp;#34; &amp;#34;$NONCE&amp;#34; &amp;#34;$LABEL&amp;#34;)

    log_message &amp;#34;Request Body: $REQUEST_BODY&amp;#34;

    SIGNATURE=$(echo -n &amp;#34;$REQUEST_BODY&amp;#34; | openssl dgst -sha512 -sign &amp;#34;$PRIVATE_KEY_PATH&amp;#34; | base64 | tr -d &amp;#39;\n&amp;#39;)

    log_message &amp;#34;Generated signature: $SIGNATURE&amp;#34;

    RESPONSE=$(curl -s -X POST &amp;#34;$AUTH_URL&amp;#34; \
        -H &amp;#34;Signature: $SIGNATURE&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$REQUEST_BODY&amp;#34;) || handle_error &amp;#34;Failed to connect to TransIP API&amp;#34;

    log_message &amp;#34;API-response received: $RESPONSE&amp;#34;

    ACCESS_TOKEN=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r &amp;#39;.token // empty&amp;#39;)
    if [ -z &amp;#34;$ACCESS_TOKEN&amp;#34; ]; then
        handle_error &amp;#34;The received access token is empty or invalid. Check the API-response: $RESPONSE&amp;#34;
    fi

    log_message &amp;#34;Received token: $ACCESS_TOKEN&amp;#34;
}

# Function to add or update a TXT record
add_or_update_txt_record() {
    local full_domain=&amp;#34;$1&amp;#34;
    local value=&amp;#34;$2&amp;#34;

    # Extract the subdomain part from the full domain
    local name=&amp;#34;_acme-challenge.${full_domain%%.*}&amp;#34;

    if [ -z &amp;#34;$name&amp;#34; ] || [ -z &amp;#34;$value&amp;#34; ]; then
        log_message &amp;#34;ERROR: Name or value for TXT record is missing.&amp;#34;
        exit 1
    fi

    # Check if the DNS record already exists
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Search for all existing records with the same name and type TXT
    MATCHING_RECORDS=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r --arg name &amp;#34;$name&amp;#34; &amp;#39;[.dnsEntries[] | select(.name==$name and .type==&amp;#34;TXT&amp;#34;)]&amp;#39;)

    # Check if there are multiple or no matching records
    MATCHING_COUNT=$(echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq &amp;#39;length&amp;#39;)

    # Ensure MATCHING_COUNT is always an integer
    if ! [[ &amp;#34;$MATCHING_COUNT&amp;#34; =~ ^[0-9]&#43;$ ]]; then
        MATCHING_COUNT=0
    fi

    if [ &amp;#34;$MATCHING_COUNT&amp;#34; -eq 0 ]; then
        log_message &amp;#34;No existing TXT record found for $name. Creating a new record.&amp;#34;
    elif [ &amp;#34;$MATCHING_COUNT&amp;#34; -ge 1 ]; then
        log_message &amp;#34;Multiple or one existing TXT record found for $name. Removing all existing records.&amp;#34;

        # Remove all existing records using the full details of each record
        echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq -c &amp;#39;.[]&amp;#39; | while read -r record; do
            existing_name=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.name&amp;#39;)
            existing_expire=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.expire&amp;#39;)
            existing_content=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.content&amp;#39;)

            # Log the details of the record to be removed
            log_message &amp;#34;Remove record: name=$existing_name, expire=$existing_expire, content=$existing_content&amp;#34;

            # Make the DELETE call using the full DNS-entry details
            DELETE_ENTRY=$(jq -n \
            --arg name &amp;#34;$existing_name&amp;#34; \
            --arg expire &amp;#34;$existing_expire&amp;#34; \
            --arg type &amp;#34;TXT&amp;#34; \
            --arg content &amp;#34;$existing_content&amp;#34; \
            &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

            DELETE_RESPONSE=$(curl -s -X DELETE \
                -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
                -H &amp;#34;Content-Type: application/json&amp;#34; \
                -d &amp;#34;$DELETE_ENTRY&amp;#34; \
                &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

            # Log the response of the DELETE call
            log_message &amp;#34;Response after removing $existing_name: $DELETE_RESPONSE&amp;#34;

            # Check if the removal was successful
            if [[ &amp;#34;$DELETE_RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
                log_message &amp;#34;ERROR: The record $existing_name could not be removed.&amp;#34;
            else
                log_message &amp;#34;Record $existing_name successfully removed.&amp;#34;
            fi
        done
    fi

    # Create a new record
    DNS_ENTRY=$(jq -n \
    --arg name &amp;#34;$name&amp;#34; \
    --arg expire &amp;#34;$TTL&amp;#34; \
    --arg type &amp;#34;TXT&amp;#34; \
    --arg content &amp;#34;$value&amp;#34; \
    &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

    RESPONSE=$(curl -s -X POST \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$DNS_ENTRY&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    log_message &amp;#34;API-response for TXT record creation: $RESPONSE&amp;#34;

    if [[ &amp;#34;$RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
        handle_error &amp;#34;The TXT record could not be created. Check the API-response: $RESPONSE&amp;#34;
    else
        log_message &amp;#34;TXT record successfully added for $name with value $value&amp;#34;
    fi
}

# Function to update an existing TXT record
update_txt_record() {
    local name=&amp;#34;$1&amp;#34;
    local value=&amp;#34;$2&amp;#34;

    DNS_ENTRY=$(jq -n \
    --arg name &amp;#34;$name&amp;#34; \
    --arg expire &amp;#34;$TTL&amp;#34; \
    --arg type &amp;#34;TXT&amp;#34; \
    --arg content &amp;#34;$value&amp;#34; \
    &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

    RESPONSE=$(curl -s -X PATCH \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$DNS_ENTRY&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    log_message &amp;#34;API-response for TXT record update: $RESPONSE&amp;#34;

    if [[ &amp;#34;$RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
        handle_error &amp;#34;The TXT record could not be updated. Check the API-response: $RESPONSE&amp;#34;
    else
        log_message &amp;#34;TXT record successfully updated for $name with value $value&amp;#34;
    fi
}

# Function to remove a TXT record for `dns-01` validation
remove_txt_record() {
    local full_domain=&amp;#34;$1&amp;#34;

    # Extract the subdomain part from the full domain
    local name=&amp;#34;_acme-challenge.${full_domain%%.*}&amp;#34;

    # Check if the DNS record exists
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Search for all records with the same name and type TXT
    MATCHING_RECORDS=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r --arg name &amp;#34;$name&amp;#34; &amp;#39;[.dnsEntries[] | select(.name==$name and .type==&amp;#34;TXT&amp;#34;)]&amp;#39;)

    # Check if there are existing records that need to be removed
    MATCHING_COUNT=$(echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq &amp;#39;length&amp;#39;)

    if [ &amp;#34;$MATCHING_COUNT&amp;#34; -eq 0 ]; then
        log_message &amp;#34;No existing TXT record found for $name to remove.&amp;#34;
    else
        log_message &amp;#34;Removing all existing TXT records for $name.&amp;#34;

        # Loop through all found records and remove them
        echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq -c &amp;#39;.[]&amp;#39; | while read -r record; do
            existing_name=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.name&amp;#39;)
            existing_expire=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.expire&amp;#39;)
            existing_content=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.content&amp;#39;)

            # Make the DELETE call using the full details
            DELETE_ENTRY=$(jq -n \
            --arg name &amp;#34;$existing_name&amp;#34; \
            --arg expire &amp;#34;$existing_expire&amp;#34; \
            --arg type &amp;#34;TXT&amp;#34; \
            --arg content &amp;#34;$existing_content&amp;#34; \
            &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

            DELETE_RESPONSE=$(curl -s -X DELETE \
                -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
                -H &amp;#34;Content-Type: application/json&amp;#34; \
                -d &amp;#34;$DELETE_ENTRY&amp;#34; \
                &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

            # Check if the removal was successful
            if [[ &amp;#34;$DELETE_RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
                log_message &amp;#34;ERROR: The TXT record $existing_name could not be removed.&amp;#34;
            else
                log_message &amp;#34;TXT record $existing_name successfully removed.&amp;#34;
            fi
        done
    fi
}

# Always generate a new token for each run
generate_new_token

# Certbot passes these values when adding and removing the record
if [ &amp;#34;$1&amp;#34; == &amp;#34;add&amp;#34; ]; then
    add_or_update_txt_record &amp;#34;$CERTBOT_DOMAIN&amp;#34; &amp;#34;$CERTBOT_VALIDATION&amp;#34;
    sleep 25  # Wait for DNS update to propagate
elif [ &amp;#34;$1&amp;#34; == &amp;#34;clean&amp;#34; ]; then
    remove_txt_record &amp;#34;$CERTBOT_DOMAIN&amp;#34;
fi</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="steps-to-run">Stappen om te draaien</h2>
<ol>
<li><strong>Download de private key</strong>: Sla het bestand met de private key (<code>transip_private_key.pem</code>) uit de API-instellingen van TransIP op op het apparaat waar je dit gaat draaien.</li>
<li><strong>Stel Docker Compose in</strong>: Zet de bestanden <code>docker-compose.yml</code> en <code>certbot-transip.sh</code> in dezelfde map. (of pas het script aan)</li>
<li><strong>Start de container</strong>: Start de container (cd naar het pad waar je yml staat)</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">docker-compose up --build</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Houd de logoutput in de gaten: de logoutput van de Docker container laat de voortgang van de DNS-challenges en het aanmaken van het certificaat zien.</p>
<p>Meer is het niet!</p>
<p>Binnenkort post ik ook een versie met een werkende Helm chart. Die was wat lastiger om alles correct te laten werken en alle certificaten te laten updaten via verschillende namespaces, waar ik de certificaten op verschillende domeinen beschikbaar moest hebben.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deprecated: Geef je headless (Raspberry Pi) VPN een boost met ProtonVPN en WireGuard</title>
      <link>https://www.allsubjectsmatter.nl/nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/</link>
      <guid isPermaLink="false">66f3e524e2acb1000109843a</guid>
      <pubDate>Wed, 25 Sep 2024 22:24:00 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Deprecated: Beheer ProtonVPN op je headless device zoals een Raspberry Pi moeiteloos met dit Python-script. Maak automatisch verbinding met de snelste server via WireGuard voor optimale snelheid en veiligheid. Dit omzeilt error 422 met de protonvpn-cli client, die op dit moment kapot is.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>De API: <a href="https://api.protonvpn.ch/vpn/logicals" data-external>https://api.protonvpn.ch/vpn/logicals</a> is volgens Proton Support officieel stopgezet. Het enige wat hij nog teruggeeft is:</p>
<pre class="line-numbers language-json"><code class="language-json">{&amp;#34;Code&amp;#34;:0,&amp;#34;Error&amp;#34;:&amp;#34;Invalid access token&amp;#34;,&amp;#34;Details&amp;#34;:{}}</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Dit artikel is daarmee helaas achterhaald. Iemand is een petitie begonnen om deze functionaliteit terug te krijgen.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://protonmail.uservoice.com/forums/932836-proton-vpn/suggestions/49774472-re-enable-access-to-api" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Re-enable access to API</span><span class="bookmark__description">Tot voor kort kon je via de API (https://api.protonvpn.ch/vpn/logicals) servers filteren op onder meer land en drukte, zodat je verbinding maakt met een server die niet overbelast is. Die mogelijkheid is verdwenen. 27 stemmen, 4 reacties.</span><span class="bookmark__meta"><span>Proton VPN feedback</span></span></span></a></figure>

<h2 id="the-deprecated-guide-which-no-longer-functions-as-of-062025-for-historical-reasons">De achterhaalde handleiding die niet meer werkt sinds 06/2025. Voor de historie bewaard.</h2>
<p>Toen ProtonVPN zijn command-line interface (CLI)-tool stopzette, bleven gebruikers van headless systemen zoals de Raspberry Pi zonder een eenvoudige manier om VPN-verbindingen te beheren. Zonder grafische interface is het handmatig wisselen van VPN-servers een gedoe geworden. Ook het selecteren van de snelste VPN uit een pool van servers is onmogelijk geworden.</p>
<p>Om dit probleem op te lossen, heb ik een Python-script ontwikkeld dat automatisch de snelste ProtonVPN-server selecteert op basis van latency en load, en daarna je WireGuard-configuratie voor je bijwerkt. Deze handleiding neemt je mee door het instellen van WireGuard voor ProtonVPN, het installeren van het script en het automatiseren van je VPN-verbinding, zodat je Raspberry Pi altijd verbonden is met de beste server.</p>
<h3 id="why-do-i-need-this-protonss-script">Waarom heb ik dit Protonss-script nodig?</h3>
<p>Dit Python-script automatiseert het verbinden van je Raspberry Pi met de snelst beschikbare ProtonVPN-server, met <strong>WireGuard</strong> als VPN-protocol. Het controleert de latency en load van meerdere VPN-servers via de API van ProtonVPN, kiest de server met de beste prestaties, werkt je WireGuard-configuratie bij met de nieuwe servergegevens en maakt alleen opnieuw verbinding als er een duidelijk snellere server is gevonden (niemand zit eigenlijk te wachten op reconnects). Het script logt het hele proces, wat het debuggen een stuk makkelijker maakt.</p>
<ol>
<li><strong>Geen handmatig VPN-wisselen meer</strong>: Zonder de CLI van ProtonVPN zou het wisselen naar de snelste server handwerk betekenen. Dit script automatiseert dat proces door regelmatig de beschikbare servers te controleren en de verbinding bij te werken op basis van realtime prestaties.</li>
<li><strong>Optimaliseert de verbindingssnelheid</strong>: Het script zorgt dat je altijd verbonden bent met de VPN-server met de laagste latency en load. Dit is vooral handig om snelle verbindingen te behouden in landen met snelle ProtonVPN-servers of servers met weinig verkeer.</li>
<li><strong>Vermindert downtime</strong>: Door het herverbinden te automatiseren, minimaliseert het script mogelijke downtime door slechte serverprestaties of trage verbindingen.</li>
<li><strong>Gemaakt voor de Raspberry Pi</strong>: Als je een headless Raspberry Pi draait (zonder monitor), is dit script ideaal om VPN-taken te automatiseren zonder constante supervisie.</li>
<li><strong>Beveiliging &amp; privacy</strong>: Door ervoor te zorgen dat je Raspberry Pi altijd via een VPN verbinding maakt, beschermt het je data en online activiteit, zeker als je de Pi gebruikt voor torrenting of als netwerkapparaat.</li>
</ol>
<p>Dit script automatiseert dus het vinden van de optimale ProtonVPN-server, met hogere snelheden, meer betrouwbaarheid en gemak voor headless systemen zoals een Raspberry Pi.</p>
<h2 id="lets-dive-in">Aan de slag!</h2>
<p>Eerst moet je WireGuard installeren, een snel en lichtgewicht VPN-protocol dat we gebruiken om verbinding te maken met onze ProtonVPN-server.</p>
<pre class="line-numbers language-bash"><code class="language-bash"># First, update your package list and install WireGuard:
sudo apt update
sudo apt install wireguard curl resolvconf</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p><strong>Genereer een WireGuard-configuratie via ProtonVPN</strong></p>
<ul>
<li>Log in op je ProtonVPN-account op hun website.</li>
<li>Ga naar het gedeelte &ldquo;Downloads&rdquo; in je dashboard.</li>
<li>Genereer een nieuw configuratiebestand onder &ldquo;WireGuard Configurations&rdquo;.</li>
<li>Gebruik je torrents, schakel dan <strong>NAT-PMP</strong> in.</li>
</ul>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_e1bb7171adce48a.webp" alt="" width="560" height="368" data-zoom-src="https://www.allsubjectsmatter.nl/nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_1345a69245a3f8c.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Sla dit configuratiebestand op je Raspberry Pi op:</p>
<pre class="line-numbers language-bash"><code class="language-bash"># Save the configuration file to your Raspberry Pi:
# Place it in the WireGuard directory and rename it for simplicity:
sudo nano /etc/wireguard/protonvpn.conf</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p>Zodra je het configuratiebestand in de juiste map hebt geplaatst, controleer je de verbinding.</p>
<pre class="line-numbers language-bash"><code class="language-bash"># Verify connection to ProtonVPN:
sudo wg-quick up protonvpn

# This should give an ip address different to the one frome your isp
curl ifconfig.me 

# Verify connection to ProtonVPN:
sudo wg-quick down protonvpn

curl ifconfig.me </code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Als deze twee ip-adressen van ifconfig.me verschillen, ben je verbonden via een VPN. Ook als je je eigen ip-adres niet kent omdat het een DHCP-adres of iets dergelijks is.</p>
<h3 id="installing-our-python-environment-for-protonss">Onze Python-omgeving installeren voor Protonss.</h3>
<p>Bij het ontwikkelen en draaien van Python-scripts op je Raspberry Pi is het verstandig om een <strong>Python virtual environment</strong> te gebruiken. Zo&rsquo;n virtuele omgeving laat je Python-packages geïsoleerd installeren en beheren, zodat de standaard Python-configuratie van je systeem onaangeroerd blijft.</p>
<p>Veranderingen in je omgeving beïnvloeden dan alleen het project waaraan je werkt — zoals het installeren van specifieke packages zoals <code>requests</code> — zonder conflicten te veroorzaken met andere Python-projecten of systeemafhankelijkheden.</p>
<p>Zo stel je een virtuele omgeving in voor ons script:</p>
<ol>
<li><strong>Installeer de virtuele omgeving</strong>: Installeer eerst het package <code>python3-venv</code>, waarmee je virtuele omgevingen kunt aanmaken en beheren.</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt install -y python3-venv python3-full</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol start="2">
<li><strong>Maak een virtuele omgeving</strong>: Na installatie kun je een virtuele omgeving aanmaken. Dit genereert een geïsoleerde omgeving in de door jou opgegeven map (in dit geval <code>~/protonss</code>).</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">python3 -m venv ~/protonss</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol start="3">
<li><strong>Activeer de virtuele omgeving</strong>: Na het aanmaken moet je de omgeving activeren. Dit zorgt dat je systeem de Python-versie en packages uit de virtuele omgeving gebruikt in plaats van de systeembrede Python-installatie.</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">source ~/protonss/bin/activate</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ul>
<li>Nu de omgeving actief is, wordt elke Python-package die je installeert (zoals <code>requests</code>) in deze geïsoleerde omgeving geïnstalleerd.</li>
</ul>
<pre class="line-numbers language-bash"><code class="language-bash">pip install requests</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p><strong>Deze virtuele omgeving in de toekomst gebruiken</strong>: Elke keer dat je Python-scripts wilt draaien die van deze virtuele omgeving afhankelijk zijn, moet je hem opnieuw activeren met het commando <code>source ~/venv/bin/activate</code>. Eenmaal actief draaien alle Python-commando&rsquo;s en packages in deze omgeving.</p>
<h3 id="installing-our-protonss-script-to-automate-a-connection-to-the-fastest-vpn">Ons ProtonSS-script installeren om automatisch met de snelste VPN te verbinden.</h3>
<p>We maken een configuratiebestand aan met alle variabelen die het Python-script gebruikt. Daarin definieer je bijvoorbeeld het pad naar je WireGuard-configuratie, het logbestand en het aantal pings om latency te testen.</p>
<pre class="line-numbers language-bash"><code class="language-bash">cd ~/protonss</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Voorbeeld <code>config.ini</code>:</p>
<pre class="line-numbers language-bash"><code class="language-bash">nano config.ini</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-ini"><code class="language-ini">[Settings]
# Path to your WireGuard configuration file
wg_config_path = /etc/wireguard/protonvpn.conf

# Path to your log file
log_file = /var/log/wireguard_updater.log

# Number of pings to measure latency
ping_count = 3

# Maximum score difference before reconnection 0 - 100
# Average score of servers is between 10-40 it seems. so setting this to high results in no switches from servers
score_threshold = 5.0

# Comma-separated list of preferred countries
preferred_countries = US

# Comma-separated list of preferred cities
# If specified, servers must be in these cities (unless they&amp;#39;re in a country without any specified city)
# Preferred cities to connect to. If specified, servers must be in these cities
# (unless they&amp;#39;re in a country without any specified city).
# Example: preferred_cities = Phoenix, Los Angeles, Seattle
# This allows you to target specific cities within your preferred countries.
# If left empty, any city within the preferred countries will be considered.
# Note: City names are case sensitive and must match ProtonVPN&amp;#39;s naming exactly.
# Multiple cities should be comma-separated.
preferred_cities =Phoenix

# Weight for latency in the server score
# latency is subjective since this is probably measured through the vpn and only counts as server latency.
# Checking the latency without a vpn isnt recommended since the proton servers wont reply back without vpn.
# Value cant be higher then 1. 0.3 translates to 30% of the score_treshold.
latency_weight = 0.3

# Weight for server load in the server score
# Load is the best factor 0.7 translates to 70% of the weight of the score_threshold. this cant be higher then 1.
load_weight = 0.7

# Enable or disable pinging servers (True or False)
# Since ping might not be reliable enough for you to keep on switching servers this can be disabled here.
ping_enabled = True

# Specify the interface to use for ping tests. By setting this option you can ping to the fastest vpn server
# this will ensure the lowest latency VPN from your location.
ping_interface = eth0

[Features]
# Choose which type of servers you want to use
# Options: p2p, plus, secure_core, tor
# Default is p2p if not specified
feature_type = p2p

# Feature explanation:
# Each server has exactly ONE feature number:
# 1, 17 = Secure Core servers
# 2 = TOR servers
# 0, 8, 16, 24 = Plus-only servers
# 4, 12, 20, 28 = P2P servers (these are also Plus servers)</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Je kunt de variabelen naar behoefte aanpassen. Laat je <code>preferred_countries</code> leeg, dan controleert het script een HELEBOEL! servers, waardoor het script erg lang gaat draaien!</p>
<h4 id="create-the-python-script"><strong>Maak het Python-script aan</strong></h4>
<p>Nu gaan we het Python-script maken dat je VPN-verbinding automatiseert. Eerst moeten we een paar stappen voorbereiden.</p>
<p>De eerste regel van het script moet een <strong>shebang</strong> zijn, die er altijd voor zorgt dat het script in de juiste Python-omgeving draait:</p>
<pre class="line-numbers language-bash"><code class="language-bash">source ~/protonss/bin/activate
which python</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Dit geeft je het pad dat je op de eerste regel van het script moet zetten.</p>
<pre class="line-numbers language-text"><code class="language-text">/home/pi/venv/bin/python</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Pas het script aan door een <strong>shebang</strong> toe te voegen: zet de volgende regel helemaal bovenaan je Python-script. Vervang <code>/home/{user}/venv/bin/python</code> door het daadwerkelijke pad dat je kreeg toen je eerder het commando <code>which python</code> uitvoerde. Laat de <code>#!</code> aan het begin van de regel staan—daaruit maakt het systeem op welke interpreter het script moet draaien. We hebben het absolute pad nodig, dus ~/ werkt niet.</p>
<pre class="line-numbers language-bash"><code class="language-bash">nano protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-python"><code class="language-python">#!/home/{user}/protonss/bin/python

import requests
import subprocess
import os
import sys
import configparser
import logging
import re
from datetime import datetime
import math
import shutil

# Get the directory of the current script
script_dir = os.path.dirname(os.path.abspath(__file__))

# Load the config file from the same directory as the script
config = configparser.ConfigParser()
config.read(os.path.join(script_dir, &amp;#39;config.ini&amp;#39;))

WG_CONFIG_PATH = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;wg_config_path&amp;#39;)
LOG_FILE = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;log_file&amp;#39;)
PING_COUNT = config.getint(&amp;#39;Settings&amp;#39;, &amp;#39;ping_count&amp;#39;)
SCORE_THRESHOLD = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;score_threshold&amp;#39;, fallback=5.0)
PREFERRED_COUNTRIES = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;preferred_countries&amp;#39;, fallback=&amp;#39;&amp;#39;).split(&amp;#39;,&amp;#39;)
PREFERRED_CITIES = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;preferred_cities&amp;#39;, fallback=&amp;#39;&amp;#39;).split(&amp;#39;,&amp;#39;)
PING_INTERFACE = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;ping_interface&amp;#39;, fallback=None)

# Weighting factors for latency and load from the config
LATENCY_WEIGHT = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;latency_weight&amp;#39;, fallback=0.3)
LOAD_WEIGHT = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;load_weight&amp;#39;, fallback=0.7)

# Ping option from config
PING_ENABLED = config.getboolean(&amp;#39;Settings&amp;#39;, &amp;#39;ping_enabled&amp;#39;, fallback=True)

# Clean up country codes (remove spaces and convert to uppercase)
PREFERRED_COUNTRIES = [country.strip().upper() for country in PREFERRED_COUNTRIES if country.strip()]

# Clean up city names (remove spaces)
PREFERRED_CITIES = [city.strip() for city in PREFERRED_CITIES if city.strip()]

# Set up logging
logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format=&amp;#39;%(asctime)s [%(levelname)s] %(message)s&amp;#39;,
    datefmt=&amp;#39;%Y-%m-%d %H:%M:%S&amp;#39;
)

logging.info(&amp;#39;--- ProtonVPN Auto Connect Script Start ---&amp;#39;)

# Feature definitions
FEATURE_SECURE_CORE = [1, 17]  # Secure Core servers
FEATURE_TOR = [2]         # TOR servers
FEATURE_PLUS = [0, 8, 16, 24]  # Plus-only servers
FEATURE_P2P = [4, 12, 20, 28]    # P2P servers (these are also Plus servers)
# check https://api.protonmail.ch/vpn/logicals for more info. 
# be aware that the secure core and tor server are very limited so check if one is available for your country as stated in the config.ini file.
# FREE servers are not included in the api response. so they are already left out.

def get_feature_description(features):
    # If no feature type is specified in config.ini, default to P2P
    # Features is a number representing server capabilities
    if not features:  # If features is 0 or None
        return &amp;#34;P2P&amp;#34;  # Default to P2P
    elif features in FEATURE_SECURE_CORE:
        return &amp;#34;Secure Core&amp;#34;
    elif features in FEATURE_TOR:
        return &amp;#34;TOR&amp;#34; 
    elif features in FEATURE_P2P:
        return &amp;#34;P2P&amp;#34;
    elif features in FEATURE_PLUS:
        return &amp;#34;Plus&amp;#34;
    else:
        return &amp;#34;P2P&amp;#34;  # Default to P2P for any unrecognized feature

def get_wanted_features():
    &amp;#34;&amp;#34;&amp;#34;Get the wanted feature type based on config priorities&amp;#34;&amp;#34;&amp;#34;
    feature_type = config.get(&amp;#39;Features&amp;#39;, &amp;#39;feature_type&amp;#39;, fallback=&amp;#39;p2p&amp;#39;).lower()
    
    if feature_type == &amp;#39;secure_core&amp;#39;:
        return FEATURE_SECURE_CORE
    elif feature_type == &amp;#39;tor&amp;#39;:
        return FEATURE_TOR
    elif feature_type == &amp;#39;plus&amp;#39;:
        return FEATURE_PLUS
    else:  # Default to P2P
        return FEATURE_P2P

# Function to fetch the list of servers from the ProtonVPN API
def get_servers(full_list=False):
    try:
        url = &amp;#39;https://api.protonmail.ch/vpn/logicals&amp;#39;
        response = requests.get(url)
        response.raise_for_status()
        data = response.json()
        logging.info(&amp;#34;API response data received.&amp;#34;)
        return data[&amp;#39;LogicalServers&amp;#39;]
    except requests.RequestException as e:
        logging.error(f&amp;#39;Error fetching server list: {e}&amp;#39;)
        sys.exit(1)
    except ValueError as e:
        logging.error(f&amp;#39;Error parsing JSON: {e}&amp;#39;)
        sys.exit(1)

# Function to filter P2P servers and servers in preferred countries
def filter_servers(servers):
    filtered_servers = []
    
    # Get feature preferences from config (nu maar één type)
    wanted_features = get_wanted_features()
    
    logging.info(f&amp;#34;Filtering servers with preferences - Countries: {PREFERRED_COUNTRIES or &amp;#39;Any&amp;#39;}, &amp;#34;
                f&amp;#34;Cities: {PREFERRED_CITIES or &amp;#39;Any&amp;#39;}, Features: {[get_feature_description(f) for f in wanted_features]}&amp;#34;)
    
    for server in servers:
        if not isinstance(server, dict):
            logging.error(f&amp;#34;Unexpected type for server: {type(server)} - Value: {server}&amp;#34;)
            continue
            
        features = server.get(&amp;#39;Features&amp;#39;, 0)
        country = server[&amp;#39;ExitCountry&amp;#39;]
        city = server.get(&amp;#39;City&amp;#39;, &amp;#39;&amp;#39;)

        # Check if server features match our wanted features
        if features in wanted_features:
            # Check country and city preferences
            if country in PREFERRED_COUNTRIES:
                if not PREFERRED_CITIES or city in PREFERRED_CITIES:
                    filtered_servers.append(server)
                    logging.info(f&amp;#34;Server matched - Country: {country}, City: {city}, &amp;#34;
                              f&amp;#34;Features: {get_feature_description(features)}&amp;#34;)
            
    if not filtered_servers:
        logging.warning(f&amp;#34;No servers found matching filters. Countries: {PREFERRED_COUNTRIES}, &amp;#34;
                      f&amp;#34;Cities: {PREFERRED_CITIES}, Features: {[get_feature_description(f) for f in wanted_features]}&amp;#34;)
    else:
        logging.info(f&amp;#34;Found {len(filtered_servers)} matching servers&amp;#34;)
        
    return filtered_servers

# Function to measure latency by pinging the server (optional)
def get_latency(hostname):
    if not PING_ENABLED:
        logging.info(f&amp;#34;Ping disabled, skipping latency check for {hostname}.&amp;#34;)
        return 0  # Return 0 latency if ping is disabled

    latencies = []
    for i in range(PING_COUNT):
        try:
            # Add the interface option if it&amp;#39;s specified
            ping_command = [&amp;#39;ping&amp;#39;, &amp;#39;-c&amp;#39;, &amp;#39;1&amp;#39;, &amp;#39;-W&amp;#39;, &amp;#39;1&amp;#39;, hostname]
            if PING_INTERFACE:
                ping_command.insert(1, &amp;#39;-I&amp;#39;)
                ping_command.insert(2, PING_INTERFACE)

            result = subprocess.run(ping_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if result.returncode == 0:
                output = result.stdout.decode()
                latency_line = [line for line in output.split(&amp;#39;\n&amp;#39;) if &amp;#39;time=&amp;#39; in line][0]
                latency = float(latency_line.split(&amp;#39;time=&amp;#39;)[1].split(&amp;#39; ms&amp;#39;)[0])
                latencies.append(latency)
            else:
                latencies.append(float(&amp;#39;inf&amp;#39;))
        except Exception as e:
            latencies.append(float(&amp;#39;inf&amp;#39;))
            logging.error(f&amp;#39;Ping error for {hostname}: {e}&amp;#39;)

    # Return the lowest (best) latency value
    best_latency = min(latencies)
    
    # Handle inf latency
    if math.isinf(best_latency):
        logging.warning(f&amp;#34;Latency for {hostname} is inf. Assigning high latency value.&amp;#34;)
        best_latency = 1000  # Assign a high latency value

    return best_latency

# Function to select the best server based on a weighted combination of latency and load
def select_best_server(servers):
    best_server = None
    best_score = float(&amp;#39;inf&amp;#39;)
    
    for server in servers:
        server_load = server.get(&amp;#39;Load&amp;#39;)
        
        # Handle inf load
        if server_load is None or math.isinf(server_load):
            logging.warning(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} has inf load. Assigning high load value.&amp;#34;)
            server_load = 100  # Assign a high load value if it&amp;#39;s inf or None
        else:
            server_load = int(server_load)
        
        for srv in server[&amp;#39;Servers&amp;#39;]:
            exit_ip = srv.get(&amp;#39;ExitIP&amp;#39;)
            hostname = exit_ip

            # Get latency, only if ping is enabled
            latency = get_latency(hostname)

            # Combine latency and load using the configured weights
            score = (latency * LATENCY_WEIGHT) &#43; (server_load * LOAD_WEIGHT)
            
            logging.info(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} - Latency: {latency:.2f} ms, Load: {server_load}%, Score: {score:.2f}&amp;#34;)
            
            if score &amp;lt; best_score:
                best_score = score
                best_server = server
                best_server_instance = srv

    return best_server, best_server_instance, best_score

# Function to read the current server IP from the WireGuard configuration
def get_current_server_ip():
    try:
        with open(WG_CONFIG_PATH, &amp;#39;r&amp;#39;) as f:
            config_text = f.read()
        # Extract the current Endpoint IP
        match = re.search(r&amp;#39;Endpoint\s*=\s*([\d\.]&#43;):\d&#43;&amp;#39;, config_text)
        if match:
            logging.info(f&amp;#34;Current server IP found in WireGuard configuration: {match.group(1)}&amp;#34;)
            return match.group(1)
        else:
            logging.warning(&amp;#39;No current Endpoint IP found in WireGuard configuration.&amp;#39;)
            return None
    except Exception as e:
        logging.error(f&amp;#39;Error reading the WireGuard configuration: {e}&amp;#39;)
        return None

# Function to find the current server&amp;#39;s performance in the live server data
def get_current_server_performance(servers, current_ip):
    if current_ip is None:
        logging.warning(&amp;#39;Current server IP is None, skipping performance check.&amp;#39;)
        return None, None, float(&amp;#39;inf&amp;#39;)
    
    for server in servers:
        for srv in server[&amp;#39;Servers&amp;#39;]:
            if srv[&amp;#39;ExitIP&amp;#39;] == current_ip:
                server_load = server.get(&amp;#39;Load&amp;#39;)
                if server_load is None or math.isinf(server_load):
                    logging.warning(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} (IP: {current_ip}) has inf load. Assigning high load value.&amp;#34;)
                    server_load = 100  # Assign a high load value
                else:
                    server_load = int(server_load)

                latency = get_latency(current_ip) if PING_ENABLED else 0
                score = (latency * LATENCY_WEIGHT) &#43; (server_load * LOAD_WEIGHT)

                logging.info(f&amp;#34;Current server {server[&amp;#39;Name&amp;#39;]} - Latency: {latency:.2f} ms, Load: {server_load}%, Score: {score:.2f}&amp;#34;)
                return server, srv, score
    logging.warning(f&amp;#34;Current server with IP {current_ip} not found in live data.&amp;#34;)
    return None, None, float(&amp;#39;inf&amp;#39;)

# Function to find the current server in the full ProtonVPN list if not found in filtered data
def find_current_server_full_list(current_ip):
    servers = get_servers(full_list=True)
    return get_current_server_performance(servers, current_ip)

# Function to update the WireGuard configuration file
def update_wg_config(server, server_instance, best_score):
    try:
        endpoint_ip = server_instance[&amp;#39;EntryIP&amp;#39;]
        endpoint_port = 51820  # Default WireGuard port
        endpoint = f&amp;#34;{endpoint_ip}:{endpoint_port}&amp;#34;
        public_key = server_instance[&amp;#39;X25519PublicKey&amp;#39;]
        server_name = server[&amp;#39;Name&amp;#39;]

        # Read the current WireGuard configuration
        with open(WG_CONFIG_PATH, &amp;#39;r&amp;#39;) as f:
            config_text = f.read()

        # Strip comments that start with # under [Peer] before PublicKey
        config_text = re.sub(r&amp;#39;#.*\n&amp;#39;, &amp;#39;&amp;#39;, config_text, flags=re.M)

        # Update the Endpoint in the configuration
        config_text = re.sub(r&amp;#39;Endpoint\s*=.*&amp;#39;, f&amp;#39;Endpoint = {endpoint}&amp;#39;, config_text)

        # Update the PublicKey in the configuration
        config_text = re.sub(r&amp;#39;PublicKey\s*=.*&amp;#39;, f&amp;#39;PublicKey = {public_key}&amp;#39;, config_text)

        # Add the new comment indicating the chosen server and score
        config_text = re.sub(r&amp;#39;\[Peer\]\n&amp;#39;, f&amp;#39;[Peer]\n# {server_name} - Auto-generated by ProtonVPN Auto Connect - Score: {best_score:.2f}\n&amp;#39;, config_text)

        # Write the updated configuration back to the file
        with open(WG_CONFIG_PATH, &amp;#39;w&amp;#39;) as f:
            f.write(config_text)
        os.chmod(WG_CONFIG_PATH, 0o600)

        logging.info(f&amp;#34;WireGuard configuration updated with server: {server_name} ({server[&amp;#39;ExitCountry&amp;#39;]}), Endpoint: {endpoint}, PublicKey: {public_key}, Score: {best_score:.2f}&amp;#34;)
    except Exception as e:
        logging.error(f&amp;#39;Error updating the WireGuard configuration: {e}&amp;#39;)
        sys.exit(1)

# Function to restart the WireGuard interface
def is_resolvconf_available():
    &amp;#34;&amp;#34;&amp;#34;Check if resolvconf is available on the system.&amp;#34;&amp;#34;&amp;#34;
    return shutil.which(&amp;#34;resolvconf&amp;#34;) is not None

def is_wg_quick_available():
    &amp;#34;&amp;#34;&amp;#34;Check if wg-quick is available on the system.&amp;#34;&amp;#34;&amp;#34;
    return shutil.which(&amp;#34;wg-quick&amp;#34;) is not None

def restart_wg():
    interface_name = os.path.basename(WG_CONFIG_PATH).split(&amp;#39;.&amp;#39;)[0]
    
    # Check if wg-quick is available before proceeding
    if not is_wg_quick_available():
        logging.error(&amp;#34;wg-quick not found. Ensure WireGuard is installed correctly.&amp;#34;)
        sys.exit(1)

    try:
        # Check if resolvconf is available before proceeding
        if not is_resolvconf_available():
            logging.warning(&amp;#34;resolvconf not found, DNS settings may not be updated correctly.&amp;#34;)

        # Bring down the interface
        subprocess.run([&amp;#39;wg-quick&amp;#39;, &amp;#39;down&amp;#39;, interface_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        # Bring up the interface
        subprocess.run([&amp;#39;wg-quick&amp;#39;, &amp;#39;up&amp;#39;, interface_name], check=True)
        logging.info(f&amp;#39;WireGuard interface {interface_name} started.&amp;#39;)

    except subprocess.CalledProcessError as e:
        logging.error(f&amp;#39;Error starting the WireGuard interface: {e}&amp;#39;)
        sys.exit(1)

# Main function
def main():
    logging.info(&amp;#39;--- Starting server selection process ---&amp;#39;)
    
    # Fetch the list of servers
    servers = get_servers()
    if not isinstance(servers, list):
        logging.error(f&amp;#39;Unexpected type for servers: {type(servers)}&amp;#39;)
        sys.exit(1)

    # Filter the servers
    filtered_servers = filter_servers(servers)
    if not filtered_servers:
        logging.error(&amp;#39;No suitable servers available after filtering.&amp;#39;)
        sys.exit(1)

    # Get the current server&amp;#39;s IP address from the WireGuard configuration
    current_ip = get_current_server_ip()

    # Get the current server&amp;#39;s performance if it exists
    current_server, current_server_instance, current_score = get_current_server_performance(filtered_servers, current_ip)

    # If the current server is not found, check the full list of servers
    if current_server is None:
        logging.info(f&amp;#34;Current server not found in filtered list, checking full server list...&amp;#34;)
        current_server, current_server_instance, current_score = find_current_server_full_list(current_ip)

    # Select the best server based on weighted latency and load
    best_server, best_server_instance, best_score = select_best_server(filtered_servers)
    if not best_server:
        logging.error(&amp;#39;No suitable server found.&amp;#39;)
        sys.exit(1)

    if current_server:
        logging.info(f&amp;#34;Current server {current_server[&amp;#39;Name&amp;#39;]} (IP: {current_ip}) - Score: {current_score:.2f}&amp;#34;)
    
    logging.info(f&amp;#34;Selected best server {best_server[&amp;#39;Name&amp;#39;]} - Score: {best_score:.2f}&amp;#34;)
    
    if current_server and current_score &amp;lt;= best_score &#43; SCORE_THRESHOLD:
        logging.info(f&amp;#34;Current server {current_server[&amp;#39;Name&amp;#39;]} is within threshold. No switch necessary.&amp;#34;)
    else:
        logging.info(f&amp;#34;Switching to server {best_server[&amp;#39;Name&amp;#39;]} due to better performance.&amp;#34;)
        update_wg_config(best_server, best_server_instance, best_score)
        restart_wg()

    logging.info(&amp;#39;--- ProtonVPN Auto Connect Script End ---&amp;#39;)

if __name__ == &amp;#39;__main__&amp;#39;:
    main()</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h3 id="testing-the-script">Het script testen</h3>
<pre class="line-numbers language-bash"><code class="language-bash">chmod &#43;x protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Zodra het script is opgeslagen, geef je het uitvoerrechten en draai je het.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo ./protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">cat /var/log/wireguard_updater.log
tail -f /var/log/wireguard_updater.log</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Je kunt het logbestand tailen of catten om de output te controleren. Tail heeft de voorkeur, want dan kun je het blijven volgen terwijl je script draait. Open gewoon een tweede ssh-sessie met de tail om het in de gaten te houden.</p>
<pre class="line-numbers language-log"><code class="language-log">2024-09-25 22:19:10 [INFO] --- ProtonVPN Auto Connect Script Start ---
2024-09-25 22:19:10 [INFO] --- Starting server selection process ---
2024-09-25 22:19:11 [INFO] API response data received.
2024-09-25 22:19:11 [INFO] Current server IP found in WireGuard configuration: 79.135.105.20
2024-09-25 22:19:11 [WARNING] Current server with IP 79.135.105.20 not found in filtered data.
2024-09-25 22:19:11 [INFO] Current server not found in filtered list, checking full server list...
2024-09-25 22:19:12 [INFO] API response data received.
2024-09-25 22:19:13 [INFO] Current server BH#1 - Latency: 26.90 ms, Load: 10%, Score: 15.07
2024-09-25 22:19:14 [INFO] Server AO#1 - Latency: 27.10 ms, Load: 7%, Score: 13.03
2024-09-25 22:19:15 [INFO] Server AO#2 - Latency: 26.60 ms, Load: 14%, Score: 17.78
2024-09-25 22:19:16 [INFO] Server AO#3 - Latency: 27.10 ms, Load: 9%, Score: 14.43
2024-09-25 22:19:17 [INFO] Server AO#4 - Latency: 26.60 ms, Load: 16%, Score: 19.18
2024-09-25 22:19:17 [INFO] Current server BH#1 (IP: 79.135.105.20) - Score: 15.07
2024-09-25 22:19:17 [INFO] Selected best server AO#1 - Score: 13.03
2024-09-25 22:19:17 [INFO] Current server BH#1 is within threshold. No switch necessary.
2024-09-25 22:19:17 [INFO] --- ProtonVPN Auto Connect Script End ---</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">sudo cat /etc/wireguard/protonvpn.conf</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Je kunt ook controleren of alles werkt door het bestand protonvpn.conf te bekijken of door je externe ip-adres na te gaan.</p>
<pre class="line-numbers language-ini"><code class="language-ini">[Peer]
# BH#3 - Auto-generated by ProtonVPN Auto Connect - Score: 7.74</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Je ziet dan iets zoals hierboven. Deze comment wordt bijgewerkt zodra er verbinding wordt gemaakt met een andere VPN, en het ip-adres binnen de peer verandert dan uiteraard mee.</p>
<pre class="line-numbers language-bash"><code class="language-bash">curl ifconfig.me #this wil give the external ip in the cli.</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Als extra stap kun je ook je externe ip controleren. Die moet anders zijn dan het ip-adres dat je provider je geeft.</p>
<h3 id="running-this-script-every-hour">Dit script elk uur draaien</h3>
<pre class="line-numbers language-bash"><code class="language-bash">sudo crontab -e</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-text"><code class="language-text">0 * * * * cd /home/{user}/protonss &amp;amp;&amp;amp; /home/{user}/protonss/protonvpn_auto_connect.py &amp;gt;&amp;gt; /home/{user}/protonss/protonvpn_cron.log 2&amp;gt;&amp;amp;1</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Hier hebben we ingesteld dat het elk uur draait, maar je kunt het plannen zoals je wilt.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo crontab -l</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Sla de crontab op en controleer dat cron is geïnstalleerd.</p>
<h3 id="optional-block-specific-traffic-if-your-vpn-fails">Optioneel: specifiek verkeer blokkeren als je VPN uitvalt</h3>
<p>Als je er zeker van wilt zijn dat er geen verkeer je device verlaat als de VPN-verbinding uitvalt, kun je <code>iptables</code> gebruiken om verkeer te blokkeren wanneer de WireGuard-interface down is.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt-get install iptables</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Controleer dat je iptables hebt geïnstalleerd, of installeer het met het bovenstaande commando.</p>
<pre class="line-numbers language-bash"><code class="language-bash">ifconfig -a</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-text"><code class="language-text">protonvpn: flags=209&amp;lt;UP,POINTOPOINT,RUNNING,NOARP&amp;gt;  mtu 1420
        inet 10.2.1.3  netmask 255.255.255.255  destination 10.2.1.3
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1000  (UNSPEC)
        RX packets 612  bytes 101768 (99.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 765  bytes 101144 (98.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Bepaal eerst de naam van je netwerkinterface. Als je je bestand protonvpn.conf hebt genoemd, heet de interface protonvpn.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo iptables -A OUTPUT -p tcp --dport 51413 ! -o protonvpn -j REJECT --reject-with icmp-port-unreachable
sudo iptables -A OUTPUT -p udp --dport 51413 ! -o protonvpn -j REJECT --reject-with icmp-port-unreachable</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>Je kunt elke regel aan je iptables toevoegen. De 2 regels hieronder zorgen ervoor dat poort 51413 wordt geweigerd zodra de netwerkinterface met de naam protonvpn niet beschikbaar is. Er is een regel voor UDP- en voor TCP-verkeer.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo iptables -L OUTPUT -v -n</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Nadat je alle benodigde regels hebt toegevoegd, kun je ze met dit commando controleren.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt-get install iptables-persistent</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>En maak de regels persistent, zodat je ze niet bij elke reboot opnieuw hoeft toe te voegen.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo netfilter-persistent save</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><h3 id="conclusion">Conclusie</h3>
<p>Met dit Python-script is je Raspberry Pi altijd verbonden met de snelste beschikbare ProtonVPN-server, zodat je de best mogelijke VPN-prestaties hebt zonder handmatig ingrijpen. Of je je Pi nu gebruikt voor netwerken, torrenten of algemene online beveiliging: deze automatisering zorgt voor optimale prestaties en betrouwbare privacybescherming.</p>
<h3 id="update20-12-2024">Update - 20-12-2024</h3>
<p>Preferred_cities toegevoegd als optie om de selectie voor de VS te verkleinen. Dat land bevat anders ruim 2000 servers om te controleren. De performance-check kan zo in elk geval wat korter worden :-).</p>
<p>En ik heb uitgevonden wat de Feature-sectie van elke server in de API betekent. Hieronder leg ik het uit. Ik heb mijn code daarnaar aangepast om er correct mee te werken. Let op: de configuratie die je eerder hebt gemaakt moet overeenkomen met het type server waarmee je verbindt. Als je configuratie niet is ingesteld om met een Secure Core te verbinden, selecteer dan ook geen Secure Core in dit script. Sluit aan bij de ProtonVPN-configuratie die je hebt aangemaakt.</p>
<p>Uitleg van de features:</p>
<p>Elke server heeft precies één feature-nummer:</p>
<ul>
<li>1, 17 = Secure Core-servers</li>
<li>2 = TOR-servers</li>
<li>0, 8, 16, 24 = Plus-only servers</li>
<li>4, 12, 20, 28 = P2P-servers (dat lijken ook Plus-servers te zijn)</li>
</ul>
<pre class="line-numbers language-json"><code class="language-json">        {
            &amp;#34;Name&amp;#34;: &amp;#34;US-AZ#105&amp;#34;,
            &amp;#34;EntryCountry&amp;#34;: &amp;#34;US&amp;#34;,
            &amp;#34;ExitCountry&amp;#34;: &amp;#34;US&amp;#34;,
            &amp;#34;Domain&amp;#34;: &amp;#34;node-us-260.protonvpn.net&amp;#34;,
            &amp;#34;Tier&amp;#34;: 2,
            &amp;#34;Features&amp;#34;: 12,</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>Na wat meer onderzoek naar vragen over de gratis servers van ProtonVPN blijkt dat ProtonVPN de publieke API-toegang tot hun free server-endpoints heeft verwijderd. Het /vpn/servers-endpoint dat eerder informatie over gratis servers had moeten leveren, geeft nu een 404-error:</p>
<pre class="line-numbers language-json"><code class="language-json">{    &amp;#34;Code&amp;#34;: 404,    &amp;#34;Error&amp;#34;: &amp;#34;Path not found&amp;#34;,    &amp;#34;Details&amp;#34;: {}}</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Op dit moment is alleen het Plus-server-endpoint via de publieke</p>
<p>API bereikbaar: url = &lsquo;<a href="https://api.protonmail.ch/vpn/logicals" data-external>https://api.protonmail.ch/vpn/logicals</a>&rsquo;</p>
<p>Dit is waarschijnlijk een bewuste keuze van ProtonVPN. Helaas kan dit script daardoor de gratis servers niet ondersteunen.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Scrum en het leger, een ander soort missie.</title>
      <link>https://www.allsubjectsmatter.nl/nl/scrum-and-the-army-a-different-kind-of-mission/</link>
      <guid isPermaLink="false">66e14bc5aea2140001bf046b</guid>
      <pubDate>Tue, 10 Sep 2024 22:03:00 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Agile]]></category>
      <description>&lt;![CDATA[Scrum is geen quick fix. Het is als een missie plannen zoals het leger dat doet. Een sprint gaat niet over snelheid, maar over precisie, teamwork en het omgaan met het onverwachte onderweg.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/scrum-and-the-army-a-different-kind-of-mission/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Er wordt al veel geschreven over Scrum. Scrum is dood, enzovoort. Nou, als je een vacature hebt voor een &ldquo;Manager&rdquo;, een &ldquo;conciërge&rdquo; of een andere duidelijk niet-Scrum functietitel, maar besluit het een vacature voor een Product Owner of Scrum Master te noemen, dan ben jij degene die het verkloot. Alleen het beest een andere naam geven zorgt er niet voor dat het ineens als magie werkt.</p>
<p>We hebben het allemaal weleens gehoord: <strong>&ldquo;We doen Scrum, en hupsakee! We leveren in een mum van tijd waarde!&rdquo;</strong> (Spoiler alert) zo werkt het niet. Scrum is geen toverdrank die chaos in orde verandert door er wat ceremonies overheen te strooien. Het kost veel meer dan de stappen in de Scrum guide volgen om tot echte resultaten te komen. Het draait om <strong>teamwork, discipline en de juiste mensen op de juiste plek</strong> — en geloof het of niet, het verschilt niet zoveel van hoe dingen in het leger getraind worden ;-).</p>
<h2 id="sprint-equals-mission">Sprint is een missie</h2>
<p>Beginnen we nog eens met de olifant in de kamer: het woord &ldquo;sprint&rdquo;. We zijn toch niet echt aan het hardlopen? Voor mij suggereert die term nog steeds haast, maar in werkelijkheid mikken we op een <strong>doordachte, goed geplande missie</strong>. Net als in het leger gaat het niet om snelheid, maar om precisie en succes. Een goed uitgevoerde missie gooi je niet zomaar in elkaar, die wordt strategisch gepland en vereist dat iedereen zijn rol speelt en zijn steentje bijdraagt tot de finish.</p>
<p>Een missie uitvoeren vraagt om aanpassing onderweg. Je gaat onbekenden tegenkomen en je moet erop voorbereid zijn om daarmee om te gaan in plaats van ze te vermijden of negeren en je missie te verkloten.</p>
<blockquote class="pull-quote"><p>Onbekenden oplossen als een <strong><u>TEAM</u></strong> is wat een Agile/Scrum-aanpak laat werken.</p>
</blockquote>
<h2 id="specialized-but-all-round">Gespecialiseerd, maar allround</h2>
<p>In zowel Scrum-teams als militaire eenheden heeft iedereen z&rsquo;n eigen specialiteiten en sterke kanten. Maar hier komt de kicker: we leveren waarde als team. Net als in het leger, waar soldaten cross-trained worden om gaten te vullen, <strong>zijn wij ook allrounders</strong>. Als er iets misgaat, steken we niet onze handen in de lucht en noemen we het een dag. We passen ons aan en klaren de klus, samen (als team!).</p>
<h2 id="scrum-werkt-niet-zonder-de-juiste-mindset-scrum-doesne28099t-work-without-the-right-mindset">Scrum werkt niet zonder de juiste mindset {#scrum-doesn%E2%80%99t-work-without-the-right-mindset}</h2>
<p>Scrum draait niet vanzelf. Alleen Scrum aannemen zorgt niet ineens voor waarde. Ik heb het zien gebeuren. Mensen denken dat als ze de ceremonies draaien en wat Scrum-gerelateerde stappen volgen, de magie vanzelf gebeurt. Maar zonder <strong>dat je team zelf het proces managet</strong>, is het als een militaire missie draaien zonder kapitein, zonder briefing, en iedereen die maar wat aanmoddert. Misschien kom je wel tot het einde, maar wat heb je dan eigenlijk gedaan?</p>
<p>Het leger weet dit goed. Er is structuur, leiders die sturen, maar ook het vertrouwen dat elk teamlid proactief is. In Scrum heb je de juiste mix nodig: een <strong>Product Owner met een mandaat en een daadwerkelijke visie die het bedrijf ondersteunt</strong>, een <strong>Scrum Master die de flow erin houdt</strong>, en een <strong>team dat niet alleen taken uitvoert maar nadenkt over het grotere geheel</strong>. Die mindset bereik je alleen door samen te werken en als één team met elkaar te blijven praten.</p>
<h2 id="a-team-that-talks-about-the-hard-stuff">Een team dat over de moeilijke dingen praat</h2>
<p>Als team moeten we samenwerken, open met elkaar praten en <strong>de moeilijke dingen aanpakken</strong>. Ik heb het niet over makkelijke gesprekken waarin iedereen het met elkaar eens is. Ik bedoel <strong>moeilijke, ongemakkelijke discussies</strong> over wat niet werkt of niet duidelijk is, wat beter moet en hoe je daar komt. Pas dan komt de juiste geleverde waarde bovendrijven.</p>
<blockquote class="pull-quote"><p>Bespreek onbekenden tijdens je missie en pak ze aan. Een &ldquo;daily scrum&rdquo; aanhouden leert dat niet vanzelf.</p>
</blockquote>
<p>Hoe denk je dat Apple de iPhone heeft gemaakt? Jobs en Ive zaten niet samen elkaar op de schouder te kloppen. Ze discussieerden, itereerden en duwden elkaar voort tot het product <strong>goed</strong> was. En zo zouden Scrum-teams moeten werken. Je doet niet gewoon wat makkelijk of snel is. Je werkt de problemen door, daagt elkaar uit en itereert tot je het hebt. Je stelt ook veel (WAAROM-)vragen, zodat je begrijpt waar ieder vandaan komt.</p>
<h2 id="briefings-and-debriefings-are-crucial-but-only-if-you-commit-to-them">Briefings en debriefings zijn cruciaal, maar alleen als je er iets voor doet</h2>
<p>In Scrum hebben we onze <strong>Planning (briefing)</strong> en onze <strong>Review en Retrospective (debriefing)</strong>. Klinkt bekend? Je kunt niet zomaar aan een missie beginnen zonder plan, en je kunt al helemaal niet verbeteren zonder terug te kijken naar wat er mis of goed ging. Maar, en hier zit de kneep: <strong>je moet deze sessies ook echt gebruiken</strong> waarvoor ze bedoeld zijn. Te vaak worden ze overhaast gedaan of afgevinkt. In het leger kan een mislukte briefing of debrief levens kosten, in Scrum kost het je de mogelijkheid om bij de volgende missie te itereren en dingen te testen en te verbeteren. Neem ze serieus, anders zijn het gewoon lege meetings die geen waarde leveren.</p>
<h2 id="teamwork-is-everything">Teamwork is alles</h2>
<p>Uiteindelijk slagen zowel Scrum als militaire missies door <strong>teamwork</strong>. Je hebt mensen nodig die betrokken zijn, die communiceren en die voor elkaar klaarstaan. Zonder dat ben je gewoon een stel mensen die roept dat ze Scrum doen, en dat levert helemaal geen waarde op. Scrum draait niet om helden, maar om het <strong>collectieve doel</strong>. Je wint samen, je verliest samen.</p>
<h2 id="scrum-isnt-a-holy-hammer-that-solves-everything">Scrum is geen heilige hamer die alles oplost</h2>
<p>Oké, even dit: <strong>Scrum is een framework</strong>. Een door mensen bedachte structuur, bedoeld om teams efficiënt samen te laten werken. Maar Scrum doen levert niet automatisch waarde op voor je bedrijf. Je hebt de <strong>juiste mensen</strong> nodig, de <strong>juiste mindset</strong> en een duidelijke <strong>missie</strong> waar iedereen naartoe werkt. Anders ben je gewoon taken aan het afvinken zonder erover na te denken — en dat gaat mis.</p>
<p>Nog iets: pas je manier van werken aan aan wat je bedrijf nodig heeft. Scrum geeft een structuur waar je je aan kunt aanpassen. Maar wees niet bang om dingen te veranderen als dat beter werkt voor je organisatie. Het doel is een team dat graag werkt binnen de structuur en waarde levert in plaats van features.</p>
<p>Je als team, of zelfs als organisatie, moet het <strong>laten werken</strong> door de juiste mensen (met visie en mandaat) en processen op zijn plek te zetten. Doe je dat niet, dan eindig je met een noodlanding: niets opgeleverd, alleen frustratie.</p>
<p>Laat solo managers die vooral zichzelf er goed uit willen laten komen je drive naar waarde en het écht helpen van de business niet overnemen. Niet de business schaden.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Azure &#43; MySQL, MariaDB, PostgreSQL: prestaties vergeleken</title>
      <link>https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/</link>
      <guid isPermaLink="false">6605e7eafc993e0001aef0ea</guid>
      <pubDate>Fri, 29 Mar 2024 00:04:45 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Een kijkje in de databaseprestaties van Azure, tijdens een reis langs MySQL, MariaDB en PostgreSQL. Ontdek welke database de race aanvoert.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h3 id="introduction">Inleiding</h3>
<p>In deze post wil ik databaseperformance binnen Azure belichten, met een directe vergelijking tussen MySQL, MariaDB en PostgreSQL. Er is veel literatuur over het onderwerp, maar praktische tests die de performance in de praktijk meten, zijn schaars.</p>
<p>Veel discussies draaien om de verschillen tussen deze databases in plaats van om hun daadwerkelijke performance. Toen ik dit onderzoek begon, stonden niet de verschillen voorop, maar de zoektocht naar een performante database die een goed Azure-alternatief kon zijn voor onze MariaDB-monolith. Aanvankelijk dachten we dat de overstap van MariaDB naar MySQL een simpele vervanging was, met nauwelijks impact op de performance. Daar hadden we het mis!</p>
<blockquote class="pull-quote"><p>&ldquo;There are many opinions about performance, But we needed facts&rdquo;</p>
</blockquote>
<p>We zagen een enorme performance-regressie bij elke query. Performance is cruciaal voor ons, en het was verwarrend om te zien dat MySQL zo ver achterbleef bij MariaDB. Toen we dit ontdekten, hebben we zelf een MariaDB-VM in Azure opgezet. Die was weer wel performant, maar bracht veel extra onderhoud en beheer met zich mee: uptime garanderen, hot standby, backups.</p>
<p>Daarom ging ik PostgreSQL onderzoeken, in de hoop op betere performance dan MySQL bood. Met deze introductie: de tests die we hebben gedaan, met een complexe query en een set kleine queries, om de performanceverschillen tussen de drie databases en hun implementaties binnen Azure te meten. We hebben ook Docker-tests toegevoegd om te kunnen vaststellen of de performanceverschillen aan de databases liggen of dat Azure de oorzaak is.</p>
<h2 id="testing-our-complex-query">Onze complexe query testen</h2>
<p>Om de databases te beoordelen draaiden we een complexe query die data uit meerdere tabellen aggregeerde, een realistische workload voor ons systeem. Om vertrouwelijkheidsredenen hebben we de tabel- en kolomnamen in deze beschrijving aangepast. Deze query moest laten zien hoe elke database omgaat met data-aggregatie en complexe joins.</p>
<pre class="line-numbers language-sql"><code class="language-sql">WITH AggregatedSkateData AS (
SELECT
    e.id AS eventID,
    e.theme_cid AS themeCID,
    s.id AS skateID,
    p.firstName || &amp;#39; &amp;#39; || p.lastName AS participantName,
    l.label AS locationLabel,
    s.priceOffered,
    s.inspectionCompleted,
    s.registrationDate,
    s.eventStartDate,
    org.name AS organizerName,
    &amp;#39;https://skateeventimages.net/&amp;#39; || im.path AS organizerImageURL,
    s.registrationCode,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 0 OR pa_last.id IS NULL THEN sp.id END) AS partsPending,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 1 THEN sp.id END) AS partsRequested,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 2 THEN sp.id END) AS partsConfirmed,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 8 THEN sp.id END) AS partsDenied,
    MAX(CASE WHEN ti_unscanned.id IS NOT NULL THEN 1 ELSE 0 END) AS hasUnscannedTickets,
    s.isPublic
FROM
    skateEvents s
    INNER JOIN events e ON s.eventID = e.id
    INNER JOIN participants p ON p.id = e.participantID
    LEFT JOIN organizers org ON org.id = e.organizerID
    LEFT JOIN images im ON im.id = org.profileImageID
    LEFT JOIN locations l ON l.id = s.locationID
    LEFT JOIN skateParts sp ON sp.eventID = s.id
    LEFT JOIN partApprovals pa ON pa.id = sp.partApprovalID
    LEFT JOIN ticketsUnscanned ti_unscanned ON ti_unscanned.id = sp.ticketID AND ti_unscanned.scannedAt IS NULL
    LEFT JOIN partApprovalActions pa_last ON pa_last.id = (
        SELECT pa2.id
        FROM partApprovalActions pa2
        WHERE pa2.partID = pa.id
        ORDER BY pa2.id DESC
        LIMIT 1
    )
WHERE
    s.registrationDate IS NOT NULL
    AND e.cancelledAt IS NULL
    AND e.deletedAt IS NULL
    AND s.isArchived = false
    AND s.deletedAt IS NULL
GROUP BY
    e.id, e.theme_cid, s.id, p.id, p.firstName, p.lastName, org.id, im.id, l.id, l.label, s.priceOffered, s.inspectionCompleted, s.registrationDate, s.eventStartDate, org.name, im.path, s.registrationCode, s.isPublic
)
SELECT * FROM AggregatedSkateData
WHERE
    TO_CHAR(s.registrationDate, &amp;#39;YYYY-MM&amp;#39;) = &amp;#39;2024-03&amp;#39;
    OR partsDenied &#43; partsRequested &#43; partsPending &amp;gt; 0
ORDER BY s.registrationDate DESC</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h3 id="azure-complicated-query">Azure, complexe query</h3>
<p>We hebben deze tests binnen dezelfde Azure-zone gedraaid voor containers en databases, om interzone-latency te vermijden. Hieronder de specificaties en resultaten, voor de transparantie. Opvallend: bij MariaDB en MySQL hadden de performancespecificaties nauwelijks invloed op de resultaten, mits we de eerste (burstable) tier vermeden. PostgreSQL was de uitzondering, met duidelijke verbeteringen bij de runtime van grote queries.</p>
<p>Met HeidiSQL via de Azure VPN hebben we onze beste resultaten van vijf runs bepaald.</p>
<p><strong>MySQL Flexible, Zone 2 (Standard_E4ds_v5, 450 iops)</strong><br>
Found rows: 130<br>
Duration for 1 query: 13,875 sec</p>
<p><strong>MariaDB VM, Zone 2 (Standard B4s v2 (4 vcpus, 16 GiB memory))</strong><br>
Found rows: 130<br>
Duration for 1 query: 10,625 sec<br>
23,4% beter dan MySQL</p>
<p><strong>Postgres Flexible, Zone 3 (Standard_E4ds_v50 p10, 500 iops)</strong><br>
Found rows: 130<br>
Duration for 1 query: 3,953 sec<br>
71,5% beter dan MySQL</p>
<h3 id="local-docker-complicated-query">Lokale Docker, complexe query</h3>
<p>De hardware waarop we lokaal hebben getest:</p>
<p>12th Gen Intel(R) Core(TM) i9-12900H, 2900 Mhz, 14 Core(s), 20 Logical Processor(s)<br>
Installed Physical Memory (RAM) 32,0 GB</p>
<p><strong>MySQL 8.3 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 12,437 sec</p>
<p><strong>MariaDB 11.4 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 9,484 sec</p>
<p><strong>Postgres 16.2 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 3,969 sec</p>
<p>Daarmee zijn de tests met de complexe query afgerond. Geweldig om te zien dat Postgres hier zo goed presteert! Voor onze overstap naar Azure draaiden we op MariaDB. Het was behoorlijk vervelend om zo&rsquo;n slechte performance in MySQL te zien zodra we met de nieuwe setup gingen testen.</p>
<h2 id="testing-our-set-small-querys">Onze set kleine queries testen</h2>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_c11062909b9cd6ee.webp" alt="" width="560" height="171" data-zoom-src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_44990348cc6259d6.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>De volgende test vuurt meer dan 20.000 transacties per seconde af op elke database, waarmee we een feed in JSON bouwen die een bepaalde tijd nodig heeft om af te ronden. Met veel queries belasten we de database beter, wat ons een gemiddelde runtime per database geeft.
Er zit ook wat PHP-logica in, want dit is een PHP-script dat we draaien om de feed te genereren. Maar dat draait altijd op dezelfde hardware en hetzelfde docker-image, dus het verschil in runtime komt van de database.
Hiervoor hebben we onze containers naar dezelfde zone verplaatst als waar de database draait, om ook mogelijke problemen met zone-latency uit te sluiten.</p>
<p>We draaien dit script binnen ons Laravel-framework. Onze PHP draait tegen <a href="https://openswoole.com/" data-external>Swoole</a> voor de best mogelijke performance binnen PHP 8.</p>
<h3 id="azure-small-querys">Azure, kleine queries</h3>
<p>In deze test komen ook aspecten van containerperformance kijken.
We draaien onze containers en database weer in dezelfde zone in Azure (West-Europa). Binnen Azure draaien we met een stuk minder performance dan we lokaal in onze docker-omgeving tot onze beschikking hebben.</p>
<p>Daarom kunnen we deze resultaten alleen gebruiken om de verschillen binnen docker en binnen Azure zelf te vergelijken, niet tussen die twee. Die met elkaar vergelijken zegt niets door dit hardwareverschil. We willen alleen het verschil in runtime tussen de databases zien, en Docker is de baseline om te checken of de verschillen overeenkomen met wat we in Azure zien.</p>
<p>Onze node size in Azure is een standard_b2s, 2 vCPU&rsquo;s met 4 GiB RAM.</p>
<p><strong>API, Zone 2 -&gt; MariaDB VM, Zone 2</strong><br>
24,59s runtime (spot nodes)</p>
<p><strong>API, Zone 2 -&gt; MySQL Flexible, Zone 2</strong><br>
27,87s runtime (spot nodes)</p>
<p><strong>API, Zone 3 -&gt; Postgres Flexible, Zone 3</strong><br>
24,92s runtime (spot nodes)</p>
<h3 id="local-docker-small-querys">Lokale Docker, kleine queries</h3>
<p>Voor de kleine query-tests binnen lokale Docker gebruiken we een systeem met een 12th Gen Intel(R) Core(TM) i9-12900H CPU, draaiend op 2900 MHz met 14 cores en 20 logical processors, aangevuld met 32 GB RAM.</p>
<p>Deze Docker images zijn zo geconfigureerd dat ze memory-bound zijn, wat betekent dat de prestaties vooral afhankelijk waren van het beschikbare geheugen en niet beperkt werden door de CPU. De CPU-instellingen stonden op onbeperkte prestaties, zodat Docker zoveel CPU-bronnen kon gebruiken als nodig.</p>
<p>Voor elke testrun tegen een database herstartte ik de Docker container om de verbinding te verversen. Na de herstart hield ik een pauze van 30 seconden aan om er zeker van te zijn dat de container volledig gestabiliseerd en idle was. Die pauze is belangrijk voor consistentere testomstandigheden. Zodra de container klaar was, startte ik het feed generator script om de feed op te bouwen, om zo te beoordelen hoe goed onze verschillende database-setups kleine queries aankunnen onder deze gecontroleerde omstandigheden.</p>
<p><strong>API -&gt; MariaDB 11.4 Docker</strong><br>
22.88s runtime</p>
<p><strong>API -&gt; MySQL 8.3 Docker</strong><br>
23.74s runtime</p>
<p><strong>API -&gt; Postgres 16.2 Docker</strong><br>
20.77s runtime</p>
<h2 id="mariadb-query-cache">MariaDB query cache</h2>
<p>In ons eerdere gebruik van MariaDB binnen onze monolithische setup maakten we veelvuldig gebruik van de query cache. Deze functionaliteit speelde een cruciale rol bij het leveren van prestaties voor herhaalde queries. Die efficiëntie houdt stand totdat de cache herbouwd moet worden, meestal getriggerd door updates of inserts op de tabellen van de gecachte query. We doen een tweede run op onze complexe query met de query cache ingeschakeld.</p>
<p><strong>Local MariaDB Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 0,000 sec</p>
<p>De prestatiewinst van de query cache is echter niet overal even groot. Bij bijvoorbeeld gemiddeld 20.000 transacties per seconde met kleine queries zakte de runtime enigszins in. Deze variatie laat zien dat de query cache niet altijd hetzelfde niveau van optimalisatie oplevert, zeker onder hoge belasting. Maar daar later meer over.</p>
<p><strong>API -&gt; Local MariaDB Docker (query cache enabled)</strong><br>
17.30s runtime</p>
<p><strong>API, Zone 2 -&gt; MariaDB VM, Zone 2 (query cache enabled)</strong><br>
22.72s runtime (spot nodes)</p>
<p>De query cache in MariaDB werkt door de result set van SELECT queries op te slaan, samen met de querytekst. Wanneer een identieke query opnieuw wordt uitgevoerd (met dezelfde tekst en onder dezelfde database-userrechten), kan MariaDB de query-uitvoerfase overslaan en het resultaat direct uit de cache halen. Dit kan de uitvoertijd van vaak gedraaide queries die geen data wijzigen drastisch verlagen.</p>
<p>Wees voorzichtig met het gebruik van de query cache van MariaDB. Ondanks de voordelen is de query cache deprecated en standaard uitgezet in nieuwere versies van MariaDB 10.1.7 (en in MySQL vanaf versie 5.7.20), vanwege zorgen over de impact op prestaties in omgevingen met veel gelijktijdige transacties. Voor specifieke scenario&rsquo;s, met name met een overwegend read-workload, kan de query cache nog steeds aanzienlijk betere prestaties opleveren. In MySQL 8 en hoger is hij volledig verwijderd.</p>
<h2 id="observation-on-azures-flexible-servers">Observatie over de Flexible Servers van Azure</h2>
<p>Deze tests laten zien dat de prestaties van databases op de flexible servers van Azure aanzienlijk zijn verbeterd en dicht bij de prestaties van lokale Docker-omgevingen liggen. Dat is een flinke verbetering ten opzichte van de eerdere Azure database single servers, die doorgaans minder prestaties leverden door een vorm van overhead die voortkwam uit de manier waarop ze in de Azure-cloud draaiden.</p>
<h2 id="azure-latency-anomaly">Azure latency-anomalie</h2>
<p>Als bonus hebben we nog wat extra tests gedaan om de interzone-latency te meten bij het verbinden van Zone 1 of 2 met een PostgreSQL-database in Zone 3 binnen Azure.</p>
<p><strong>API, Zone 2 -&gt; Azure PostgreSQL Flexible, Zone 3</strong><br>
De runtime was 44.23 seconden.</p>
<p>Interessant: de runtime verdubbelde opnieuw, een fenomeen dat we uitsluitend zien bij communicatie tussen Zone 1-2 en Zone 3, of andersom. Dit patroon suggereert dat elke container die een databaseverbinding nodig heeft onder deze omstandigheden aanzienlijke latency ondervindt, terwijl de latency vrijwel verwaarloosbaar is bij interactie tussen Zone 1 en Zone 2.</p>
<p>Voor wie het gemist heeft: ik heb eerder een latency-anomalie in Zone 3 besproken. De gangbare wijsheid is dat je de databaseverbinding binnen dezelfde zone houdt om latency te minimaliseren. Onze tests laten echter zien dat de latency-impact minimaal is tussen Zone 1 en 2, maar aanzienlijk wordt zodra je van Zone 1 of 2 naar Zone 3 gaat.</p>
<p>Deze tests bevestigen opnieuw dat het latency-probleem niet exclusief is voor MySQL Flexible services, maar ook PostgreSQL Flexible services binnen Azure raakt, wat wijst op een breder patroon in het database-aanbod van Azure.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/azure-western-europe-zone-3-latency-issue-exploration/"><span class="bookmark__body"><span class="bookmark__title">Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</span><span class="bookmark__description">Unveiling a significant Azure performance issue: Zone 3’s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/mysql-mariadb-postgresql-performance-compared-in-azure/bookmarks/01-thumb_hu_27e6839ff9d3a962.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<h2 id="conclusion">Conclusie</h2>
<p>Vanuit prestatieoogpunt is het duidelijk dat de keuze voor de juiste database cruciaal is en moet aansluiten bij de specifieke snelheids- en efficiëntiebehoeften van je app. De resultaten laten zien dat MySQL vaak trager is. MariaDB presteert goed en heeft een handige query cache, maar Azure ondersteunt MariaDB niet meer. Daarmee is PostgreSQL de meest logische keuze voor prestatiegerichte gebruikers in Azure, want het blinkt uit in het verwerken van zowel complexe queries als grote aantallen simpele queries, zoals de tests hierboven laten zien.</p>
<p>Hopelijk helpt deze analyse bij het kiezen van de juiste databasetechnologie en onderstreept ze hoe belangrijk zorgvuldige planning is voor het verbeteren van je app-prestaties. Voor wie Azure gebruikt en topprestaties zoekt, is PostgreSQL in mijn testresultaten de beste optie.</p>
<p>Er zijn genoeg andere redenen om je database te kiezen. Maar in deze post wilde ik me alleen richten op performance. Dat was onze grootste zorg, want we zijn bezig met de overstap van een monolithische server naar Azure. Bij een database-switch komt nog meer kijken. Het voordeel hier is dat alle drie de databases zich redelijk makkelijk laten integreren, welke je ook kiest.</p>
<p>Postgres is hier het best compatibel dankzij zijn strikte routines. De gecompliceerde query hierboven is geschreven voor Postgres en draait in MariaDB zonder één wijziging. Voor MySQL hoef je alleen de TO_CHAR in de laatste regel te vervangen door DATE_FORMAT.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Latency-anomalie: een poging om Azure-performanceproblemen in West-Europa te ontrafelen</title>
      <link>https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/</link>
      <guid isPermaLink="false">65e6f9da5ce24800015cc7ae</guid>
      <pubDate>Tue, 05 Mar 2024 12:16:52 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Een groot Azure-performanceprobleem: de onverwachte latency-anomalie in Zone 3 van de regio West-Europa zet clouddeploymentstrategieën onder druk]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="introduction">Introductie</h2>
<p>We zijn begonnen aan een overstap van een traditionele VM-setup bij TransIP naar de schaalbare cloudinfrastructuur van Microsoft Azure. Die reis draait net zo goed om ontdekking als om prestaties. Met de expertise van <a href="https://www.pionative.com/" data-external>Pionative</a> als leidraad verhuizen we onze omgeving niet alleen; we proberen haar in Azure opnieuw op te bouwen, waarbij we elke stap zorgvuldig testen en analyseren. Ons doel? Optimale prestaties in deze nieuwe architectuur, en eventuele bottlenecks of verborgen problemen in onze nieuwe cloudarchitectuur zo vroeg mogelijk op het spoor komen.</p>
<h2 id="azure-regions-and-zones">Azure-regions en zones</h2>
<p>De infrastructuur van Azure beslaat de hele wereld, georganiseerd in regions voor gelokaliseerde diensten, en verder onderverdeeld in zones voor hoge beschikbaarheid. <strong>Regions</strong> zijn specifieke geografische gebieden waar Azure-diensten draaien, terwijl <strong>zones</strong> fysieke locaties binnen die regions zijn, elk met eigen voorzieningen om storingen te isoleren.</p>
<h2 id="the-western-europe-region">De region Western Europe</h2>
<p>De region Western Europe in Azure is opgebouwd uit drie aparte availability zones. Deze zones bieden een extra laag redundantie en fouttolerantie door datacenters binnen de region fysiek van elkaar te scheiden. Deze opzet moet garanderen dat zelfs bij een volledige uitval van één zone de diensten in de andere zones gewoon doordraaien — een stevige disaster recovery-oplossing voor bedrijven die in Europa actief zijn. Waar Zone 1, 2 en 3 geografisch precies liggen, is mij niet bekend.</p>
<h2 id="performance-anomaly-in-the-western-europe-region">Prestatie-anomalie in de region Western Europe</h2>
<p>Ons testprogramma bracht ons bij een anomalie die het middelpunt van onze Azure-verkenning in deze post vormt: een duidelijk latency-probleem in Zone 3 van de region Western Europe. Deze ontdekking kwam niet meteen; ze kwam uit dagen prestatietesten en scherpe observatie van patronen die afweken van wat we verwachtten. Dat komt vooral doordat we Azure nog maar net aan het leren zijn, en door het prestatieverschil met onze huidige setup.</p>
<p>Eerst hebben we onze eigen pods, code en prestatie-instellingen binnen de node pool uitgesloten. Database-prestatie-instellingen en dat soort zaken.</p>
<h2 id="methodology-and-findings">Methodiek en bevindingen</h2>
<p>Onze uiteindelijke aanpak was behoorlijk grondig om dit resultaat te verifiëren. We deployden twee node pools in elk van de drie zones van Microsoft Azure binnen de region Western Europe, om nodespecifieke verschillen uit te sluiten. Al die pools hadden exact dezelfde prestatie-instellingen.</p>
<p>Uit uitgebreide tests — waaronder een hele reeks database-aanroepen en de cron jobs die essentieel zijn voor de functionaliteit van onze applicatie — zagen we een consistent en verrassend patroon. Ongeacht de gerapporteerde capaciteit of drukte van de zone.</p>
<blockquote>
<p>Interacties met de database waren aanzienlijk trager zodra de database óf de verbindende pods in Zone 3 stonden.</p>
</blockquote>
<h2 id="performance-results">Prestatieresultaten</h2>
<p>Hieronder onze situatie met een cronjob die lokaal in Docker tegen onze huidige setup zo&rsquo;n 32 seconden nodig heeft. Die setup is niet vergelijkbaar met MS Azure, maar gaf ons wel een prestatiematrix en een doel. Het script (de cronjob) dat we draaiden vuurt zo&rsquo;n 32.000 queries af om een feed te bouwen van alle producten op onze website. Ja, dat is veel, maar daar gaat het hier niet om. We wilden het prestatieprobleem blootleggen. We weten dat dit script geoptimaliseerd kan worden om beter te presteren.</p>
<p><strong>MySQL-database in Zone 2</strong><br>
Zone 1 – pool 1: 33 seconden<br>
Zone 1 – pool 2: 32 seconden</p>
<p>Zone 2 –  pool 1:  32 seconden<br>
Zone 2 –  pool 2:  32 seconden</p>
<p>Zone 3 –  pool 1: 115 seconden<br>
Zone 3 –  pool 2: 117 seconden</p>
<p>En toen, als klap op de vuurpijl, hebben we de setup omgedraaid met een database in Zone 3. Zie hieronder de resultaten die we hoopten te zien om dit onderzoek af te sluiten.</p>
<p><strong>MySQL-database in Zone 3</strong><br>
Zone 1: 120 seconden<br>
Zone 2: 122 seconden<br>
Zone 3: 33 seconden</p>
<p>We zagen dezelfde consistentie terugkomen, elke keer opnieuw, ook toen we minder maar grotere database-aanroepen draaiden.</p>
<p><strong>MySQL-database in Zone 2</strong><br>
16 DB-queries, single threaded.</p>
<p>Zone 1: Totale tijd voor queries uit log: 9,57 ms<br>
Zone 2: Totale tijd voor queries uit log: 9 ms<br>
Zone 3: Totale tijd voor queries uit log: 49,96 ms</p>
<h2 id="website-performance-in-azure">Websiteprestaties in Azure</h2>
<p>Als we onze database in Zone 2 draaien met de API-pod in Zone 1, daalt de initiële laadtijd van onze website met minstens 400 milliseconden bij de eerste page load, vergeleken met de configuratie waarin de API-pod in Zone 3 staat.</p>
<p>Dat betekent dat de setup met de database in Zone 2 en de API-pod in Zone 1 een veel snellere initiële responstijd voor de website oplevert dan wanneer de API-pod in Zone 3 staat en de database in Zone 2.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_239c52cf6c075a2e.webp" alt="" width="560" height="129" data-zoom-src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_5c52b5671bc6fe37.webp" loading="lazy" decoding="async">
  </picture><figcaption>De performance van de eerste call op onze website, in een optimale setup in Azure.</figcaption></figure>

<p>Er zit hier een foutmarge, maar uit deze resultaten kunnen we concluderen dat je, als je je API geografisch over twee zones wilt verdelen, nooit consistent kunt presteren met Zone 3. Zones 1 en 2 zijn de juiste keuze.</p>
<p>We moeten eerlijk zijn: deze prestatie-anomalie doet zich alleen voor bij een pod met een databaseverbinding. Onze website (die geen databaseverbinding heeft) in Zone 3 draaien had vrijwel geen meetbaar effect, zolang de API-pod in Zone 1 draaide en de database in Zone 2 stond.</p>
<h2 id="diving-deeper-in-zone-3s-latency-issues">Dieper duiken in de latency-problemen van Zone 3</h2>
<p>Dit prestatieverschil in Zone 3 was voor ons verbijsterend. Volgens de best practices van Microsoft zou de fysieke nabijheid van applicatieservers bij hun databases binnen dezelfde regio de latency moeten minimaliseren. Onze bevindingen schetsten echter een ander beeld: de keuze van een zone binnen een regio kan flink invloed hebben op de performance van je applicatie.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/data-src-image-68bb083d-b85e-44ae-9919-8404530f242e_hu_f23e2d3a977b47e3.webp" alt="" width="560" height="235" data-zoom-src="https://www.allsubjectsmatter.nl/nl/azure-western-europe-zone-3-latency-issue-exploration/images/data-src-image-68bb083d-b85e-44ae-9919-8404530f242e_hu_dda4b94313ff73ba.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Ons onderzoek was behoorlijk grondig. We hebben mogelijke variabelen uitgesloten, zoals onze eigen codebase, PHP-configuratie, MySQL-performance en meer. De aanhoudende latencyproblemen, vooral bij single-threaded cron jobs in PHP, wezen uiteindelijk richting een structureel probleem in de netwerkarchitectuur van Zone 3, of ergens in de deployment.</p>
<h2 id="collaboration-and-learning">Samenwerking en leren</h2>
<p>Ons traject, gedragen door de samenwerking met <a href="https://www.pionative.com/" data-external>Pionative</a>, onderstreept hoe waardevol het is om samen te werken als je door de complexiteit van de cloud moet navigeren. Hoe dieper we in deze latency-anomalie doken, hoe duidelijker werd dat we niet alleen onze eigen deployment wilden optimaliseren, maar onze bevindingen ook wilden teruggeven aan de bredere Azure-community.</p>
<blockquote class="pull-quote"><p>Deze latency-impact is te groot om te negeren in onze productieomgeving.</p>
</blockquote>
<h2 id="implications-and-next-steps">Implicaties en vervolgstappen</h2>
<p>Deze anomalie in Zone 3 roept discussies op over de optimale zonekeuze en deploymentstrategieën binnen de regio West-Europa van Azure. Is dit de reden dat Zone 3 de dunstbevolkte zone is? Online is er hier zo te zien weinig over te vinden. Daarom heb ik deze post geschreven: het kostte ons letterlijk dagen om dit prestatieverschil op te sporen.</p>
<blockquote>
<p>Dit gaat invloed hebben op onze productieomgeving. We draaien onze DB in Zone 1 of 2, en de API moet in een van deze zones staan. Zone 3 kan gebruikt worden voor pods die met de API praten.</p>
</blockquote>
<p>Deze latency-anomalie in Zone 3 was voor ons meer dan een uitdaging. We zagen het als een kans om te leren, aan te passen en onze kennis te verbreden. Terwijl we blijven samenwerken, testen en onze aanpak verfijnen, groeit ons Azure-traject uit tot een zeer interessante technische verkenning die onze toolkit voor de toekomst verruimt.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hotfixes met Docker-artifacts in een Git Flow-strategie</title>
      <link>https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/</link>
      <guid isPermaLink="false">65d330e818945800013cc243</guid>
      <pubDate>Tue, 20 Feb 2024 09:53:55 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Agile]]></category><category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Leer hotfixes in Kubernetes beheersen met Docker. Ontdek hoe je kritieke fixes snel deployt in een CI/CD-pipeline met een strakke branching-strategie. Ideaal voor het oplossen van urgente problemen zonder de flow te verstoren. Duik erin.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Deze post is een vervolg op GIT deployment-strategieën binnen Kubernetes-omgevingen. Lees eerst deze post:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/"><span class="bookmark__body"><span class="bookmark__title">CI/CD pipeline with Docker and Git branching strategy</span><span class="bookmark__description">Explore advanced CI/CD with Docker &amp; Git. Master deploying Docker artifacts via Git branching. Dive into strategies for DEV, STAGING, LIVE environments. Perfect your pipeline now!</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/bookmarks/01-thumb_hu_4b0a9042ec0e16fc.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<p>In een CI/CD-pipeline die draait om Docker artifacts, met de focus op één enkele branch, vraagt een efficiënte hotfix-strategie om een kleine afwijking van de standaardaanpak. Zo zorgen we dat kritieke fixes snel gedeployd worden. Laten we onze aanpak voor hotfixes in deze Docker-gerichte omgeving uitwerken, op basis van de branch-architectuur uit de post hierboven.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/images/Git-branches-and-docker-environments-ci_cd-1_hu_17e1895a357c0fa7.webp" alt="" width="560" height="378" data-zoom-src="https://www.allsubjectsmatter.nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/images/Git-branches-and-docker-environments-ci_cd-1_hu_7bd9f2a51f818afa.webp" loading="lazy" decoding="async">
  </picture><figcaption>Grafische weergave van onze branching- en deploymentstrategie</figcaption></figure>

<h2 id="identifying-the-need-for-a-hotfix">Wanneer is een hotfix nodig?</h2>
<p>Hotfixes zijn cruciaal voor de stabiliteit en betrouwbaarheid van elke software in productie. Ze zijn meestal nodig in situaties die directe actie vragen. Weten wanneer je een hotfix inzet, is essentieel om je CI/CD-pipeline goed te beheren, zeker als je werkt met Docker artifacts en een gestroomlijnde branch-strategie met één hoofdbranch. Dit zijn onze scenario&rsquo;s waarin een hotfix nodig kan zijn:</p>
<ol>
<li><strong>Beveiligingslekken:</strong> Een van de meest urgente redenen voor een hotfix is het ontdekken van een beveiligingslek in je applicatie. Of het nu gaat om blootgestelde data, risico op ongeautoriseerde toegang of een andere kwetsbaarheid die kwaadwillenden kunnen misbruiken: je moet snel handelen om het lek te dichten en je gebruikers en data te beschermen.</li>
<li><strong>Kritieke bugs die de gebruikerservaring raken:</strong> Als een serieuze bug door je testproces heen glipt en in productie belandt, kan dat de gebruikerservaring flink raken. Dat kan van alles zijn: een kapotte kernfunctionaliteit waardoor gebruikers de applicatie niet meer kunnen gebruiken, of ernstige performanceproblemen die leiden tot timeouts of crashes. Raakt het probleem een groot deel van je gebruikers? Dan is een hotfix op zijn plaats.</li>
<li><strong>Compliance-problemen:</strong> Sommige applicaties hebben te maken met wettelijke of regelgevende eisen. Als er een compliance-probleem wordt geconstateerd dat je juridische positie of de privacy van gebruikers kan raken, moet dat snel met een hotfix worden opgelost om boetes of verlies van vertrouwen te voorkomen.</li>
<li><strong>Risico op dataverlies:</strong> Situaties waarin gebruikersdata verloren kan raken of corrupt kan worden, vragen om directe actie. Of het nu een fout is in hoe data wordt opgeslagen, bijgewerkt of opgehaald: deploy dan een hotfix om verlies of corruptie van gebruikersdata te beperken.</li>
<li><strong>Falende betalingen of transacties:</strong> Voor applicaties die betalingen of andere transacties verwerken, zijn problemen die transacties blokkeren of tot financiële discrepanties leiden kritiek. Zulke problemen schaden niet alleen het vertrouwen van gebruikers, maar kunnen ook financiële gevolgen hebben voor zowel gebruikers als de dienstverlener. Een hotfix is dan noodzakelijk.</li>
</ol>
<h2 id="deploying-a-hotfix">Een hotfix deployen</h2>
<p>Als er een kritiek probleem in de productieomgeving wordt geconstateerd, is het doel om zo snel mogelijk een hotfix te ontwikkelen, testen en deployen. Omdat MAIN onze hoofdbranch voor development is en we Docker artifacts genereren voor deployment, hebben we een proces nodig dat snelle interventie mogelijk maakt zonder het lopende werk te verstoren.</p>
<ol>
<li><strong>Een hotfix-branch aanmaken:</strong> In plaats van te branchen vanaf je Production branch (LIVE bestaat in deze strategie immers niet meer in de Git-repository), maak je de hotfix-branch vanaf de commit die op dat moment in productie draait. Zo is de hotfix gebaseerd op exact de versie die in de productieomgeving draait. De specifieke commit vind je via tags of Docker image-labels die gekoppeld zijn aan de productie-deployment.</li>
<li><strong>De hotfix ontwikkelen en testen:</strong> De kritieke fix wordt ontwikkeld binnen deze hotfix-branch. De urgentie vraagt om een gerichte aanpak: pak het probleem aan met minimale wijzigingen om testen en deployen te versnellen. Ook onder tijdsdruk is het naleven van de Definition of Done (DoD) cruciaal voor de stabiliteit en betrouwbaarheid van de hotfix. Dat kan een compactere versie van de DoD zijn, met prioriteit voor de kritieke tests en checks die garanderen dat de fix geen nieuwe problemen introduceert.</li>
<li><strong>Een Docker artifact voor de hotfix maken:</strong> Zodra de hotfix aan de afgesproken DoD-criteria voldoet, bouwt een geautomatiseerd proces een Docker image van de hotfix-branch. Dit Docker artifact krijgt een passende tag, zodat duidelijk is waar het voor dient en hoe urgent het is.</li>
<li><strong>De hotfix deployen:</strong> Het nieuwe Docker artifact wordt direct in de productieomgeving gedeployd, waarbij de gebruikelijke route via DEV en STAGING naar PRODUCTION wordt overgeslagen. Deze deployment is alleen te rechtvaardigen door de kritieke aard van de fix en de tests die erop zijn uitgevoerd.</li>
<li><strong>Terugmergen en consistentie bewaken:</strong> Als de hotfix succesvol in productie draait, is het essentieel om de wijzigingen terug te mergen naar de MAIN branch en eventuele andere relevante branches of tags. Pas deze stap zorgt ervoor dat de fix wordt opgenomen in het lopende developmentwerk en toekomstige releases, en dat je geen regressie krijgt!!</li>
</ol>
<h2 id="ensuring-rapid-deployment-and-minimizing-disruption">Snel deployen en zo min mogelijk storing</h2>
<ul>
<li><strong>Geautomatiseerde processen:</strong> Automatisering speelt een sleutelrol in deze strategie, van het maken van het Docker artifact tot de deployment. Geautomatiseerde tests, builds en deployments zorgen dat het hotfix-proces zo snel en efficiënt mogelijk verloopt.</li>
<li><strong>Communicatie en monitoring:</strong> Snelle deployment van hotfixes vraagt om heldere communicatie binnen het team en nauwe monitoring van de productieomgeving na de deployment, om te verifiëren dat de fix werkt en geen nieuwe problemen introduceert.</li>
<li><strong>Documentatie en review:</strong> Het documenteren van het hotfix-proces en het doen van post-mortem-reviews helpt om de hotfix-strategie te verfijnen, zodat het team bij toekomstige problemen nog effectiever kan reageren.</li>
</ul>
<h2 id="conclusion">Conclusie</h2>
<p>In een CI/CD-pipeline met Docker en Git Flow als hoofdstrategie vraagt een hotfix om een strakke aanpak: gebruik de automatisering die er al is en focus op snelle, gerichte fixes. Door hotfix-branches te baseren op de commit die in productie staat, fixes te bouwen en testen volgens een goed gedefinieerde Definition of Done, en de Docker-artifacts direct naar productie te deployen, lost een team kritische problemen snel op zonder de stabiliteit en integriteit van de applicatie in gevaar te brengen.</p>
]]></content:encoded>
    </item>
    <item>
      <title>CI/CD-pipeline met Docker en een Git branching-strategie</title>
      <link>https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/</link>
      <guid isPermaLink="false">65d3035a18945800013cc216</guid>
      <pubDate>Tue, 20 Feb 2024 09:50:55 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Agile]]></category><category>&lt;![CDATA[Tech]]></category>
      <description>&lt;![CDATA[Een kijkje in CI/CD met Docker &amp; Git. Docker-artifacts deployen via Git branching onder de knie krijgen. Strategieën voor DEV-, STAGING- en PROD-omgevingen. Maak je pipeline beter!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>In deze post ga ik in op het beheren van een CI/CD-pipeline met Docker, met de nadruk op een Git branching-strategie met een development- (DEV), staging- (STAGING) en production-branch (LIVE). Het doel is duidelijk maken welke rol elke branch speelt in het proces, en vooral hoe Docker-artifacts binnen dat stramien worden aangemaakt en beheerd.</p>
<h2 id="docker-deployment-using-git-flow-with-multiple-branches">Docker-deployments met Git Flow en meerdere branches</h2>
<p>De kern van onze branching-strategie bestond uit drie branches:</p>
<ol>
<li><strong>DEV</strong>: Hier gebeurt alle ontwikkeling. Elke nieuwe feature, bugfix of verbetering start in een feature branch, afgesplitst van DEV. Is het werk af, gereviewd en voldoet het aan de Definition of Done (DoD), dan wordt het gemerged naar DEV.</li>
<li><strong>STAGING</strong>: Deze branch spiegelt een pre-productieomgeving. Hier worden geïntegreerde features uit DEV als geheel getest en door stakeholders geaccepteerd, zodat we zeker weten dat alles samenwerkt.</li>
<li><strong>MASTER</strong>: De branch die de huidige staat van onze live-omgeving weerspiegelt. Alleen grondig geteste en door stakeholders goedgekeurde wijzigingen uit STAGING halen het tot Productie.</li>
</ol>
<figure class="bookmark"><a class="bookmark__link" href="https://dev.to/preethamsathyamurthy/git-branching-and-branching-strategy-4mci" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Git Branching and Branching Strategy</span><span class="bookmark__description">In this post, we will go over why branching is required, the difference between development, staging…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/01-icon_hu_4e78872e34382e92.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>DEV Community</span><span>Preetham</span></span></span></a></figure>

<p>Dit was onze variant op een Git Flow-strategie, die we tot nu toe gebruikten. Zodra je met artifacts gaat werken, verandert dat allemaal terug naar een normale Git Flow-structuur. We hebben nog steeds omgevingen die draaien (Dev, Staging en Prod). Het echte verschil is dat code nu één keer gecommit wordt, in plaats van gemerged te worden naar een branch die op Staging of in Productie draait.</p>
<p>In plaats daarvan hebben we nu één branch, die we Main noemen. Daar wordt na een geslaagde code merge met Docker images een runnable artifact aangemaakt. Alles wat we op Staging of in Productie deployen is een specifieke versie van een eerder aangemaakt artifact.</p>
<p>Het grootste verschil: STAGING en PROD zijn geen branches meer waar je code in merged. Het zijn omgevingen geworden die een specifieke versie van een Docker image draaien. Dat brengt eigen uitdagingen met zich mee, zeker bij een hotfix. Hotfixen krijgt een eigen post, die je na deze kunt lezen. Ik loop het nu eerst stap voor stap door, zonder de hotfix erbij. Het is al omvangrijk genoeg zo.</p>
<h2 id="what-is-an-artifact">Wat is een artifact?</h2>
<p>Een Docker-artifact, vaak door elkaar gehaald met een Docker image, is een lichtgewicht, zelfstandig uitvoerbaar package dat alles bevat wat nodig is om een stuk software te draaien: de code, een runtime, libraries, omgevingsvariabelen en configuratiebestanden. Simpel gezegd: een snapshot van je applicatie én de omgeving eromheen, samen verpakt, klaar om overal gedeployed en gedraaid te worden waar Docker is geïnstalleerd. Zo weet je dat alles in verschillende omgevingen consistent werkt.</p>
<h2 id="the-importance-of-dod-when-merging-code">Het belang van de DoD bij het mergen van code</h2>
<p>Een volledige en heldere Definition of Done (DoD) speelt in dit nieuwe proces een sleutelrol, zeker nu alle wijzigingen in één enkele branch gemerged worden die een Docker image moet opleveren die runnable en deployable is:</p>
<ul>
<li><strong>Kwaliteitsborging</strong>: De DoD zorgt dat alle wijzigingen — nieuwe features, verbeteringen en hotfixes — die terug in MAIN gemerged worden aan een consistent kwaliteitsniveau voldoen. Denk aan code quality checks, succesvol doorlopen geautomatiseerde tests en het volgen van security best practices.</li>
<li><strong>Regressie voorkomen</strong>: Door de DoD toe te passen op alle merges naar MAIN voorkomt het team regressies en zorgt het dat nieuwe wijzigingen bestaande functionaliteit niet breken. Cruciaal voor de integriteit en stabiliteit van de codebase.</li>
<li><strong>Deployments soepeler laten verlopen</strong>: Een duidelijke en volledige DoD maakt deployments makkelijker, omdat alleen volledig gecontroleerde, deploy-klare wijzigingen in MAIN gemerged worden. Zo verspil je geen resources aan het bouwen van Docker images met bugs of die helemaal niet werken, en dat levert efficiëntere en betrouwbaardere artifacts op.</li>
</ul>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/images/Git-branches-and-docker-environments-ci_cd_hu_17e1895a357c0fa7.webp" alt="" width="560" height="378" data-zoom-src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/images/Git-branches-and-docker-environments-ci_cd_hu_7bd9f2a51f818afa.webp" loading="lazy" decoding="async">
  </picture><figcaption>Hier een grafische weergave van onze Git Flow-strategie en hoe die door de verschillende omgevingen loopt.</figcaption></figure>

<h2 id="creating-and-managing-artifacts">Artifacts aanmaken en beheren</h2>
<p>De eerste voorwaarde om dit proces te laten werken: zodra een afgesplitste branch terug in MAIN gemerged wordt, start dat een geautomatiseerd proces in onze CI/CD-pipeline. Dit gebeurt er:</p>
<ol>
<li><strong>Geautomatiseerde tests</strong>: Eerst draaien de geautomatiseerde tests om te checken of de nieuwe wijzigingen aan onze kwaliteitsnormen voldoen. Cruciaal voor een stabiele branch.</li>
<li><strong>Docker-artifact aanmaken</strong>: Zijn alle tests geslaagd, dan wordt er een Docker image (ons artifact) van de MAIN-branch gebouwd. MAIN is immers de enige branch die de laatste staat van alle afgeronde ontwikkeling vertegenwoordigt. Van daaruit genereren we artifacts die &ldquo;shippable&rdquo; producten kunnen worden.</li>
<li><strong>Waarom MAIN voor Docker-artifacts?</strong>: Door artifacts op dit moment aan te maken vangen we integratieproblemen vroeg op. Omdat elke feature die in MAIN gemerged wordt als release-klaar wordt beschouwd, heb je door hier een artifact te genereren altijd een geteste en potentieel deploybare versie van je product bij de hand.</li>
</ol>
<h2 id="transferring-from-branches-to-environments">Van branches naar omgevingen</h2>
<p>In traditionele Git-workflows staan branches als STAGING en PROD voor verschillende fasen in het ontwikkelproces, van testen tot productie. In een Docker-gecentreerde CI/CD-pipeline verschuift de focus: deze branches dienen niet langer voor code-integratie, maar als referentie voor deployment-omgevingen.</p>
<h2 id="deploying-to-staging">Deployen naar STAGING</h2>
<ul>
<li><strong>Docker-artifacts aanmaken</strong>: zodra de ontwikkeling klaar is en gemerged is naar de MAIN-branch, wordt er automatisch een Docker-artifact aangemaakt. Dat artifact is een snapshot van je applicatie op dat moment, met alle wijzigingen die gemerged zijn.</li>
<li><strong>Deployen in plaats van mergen</strong>: de rol van de STAGING-omgeving is om dit Docker-artifact te testen onder omstandigheden die sterk lijken op productie. In plaats van code van MAIN naar een STAGING-branch te mergen, deploy je het Docker-artifact naar een STAGING-omgeving. De code zelf beweegt dus niet tussen branches; het is de gecompileerde en gecontaineriseerde versie van je applicatie die getest wordt.</li>
<li><strong>Het doel van STAGING</strong>: de STAGING-omgeving is essentieel voor uitgebreide tests, waaronder performance- en security-assessments. Het is een laatste check om te verifiëren dat alles werkt zoals verwacht in een productie-achtige omgeving, zonder je echte gebruikers direct te raken.</li>
</ul>
<h2 id="going-to-production">Naar productie</h2>
<ul>
<li><strong>Goedkeuring van stakeholders</strong>: zodra je Docker-artifact grondig getest is in STAGING en goedgekeurd is door de stakeholders, is het klaar voor productie. Deze stap is cruciaal om te garanderen dat alle features, fixes en updates aan de nodige standaarden en verwachtingen voldoen.</li>
<li><strong>Deployen naar productie</strong>: de overgang van STAGING naar PROD betekent dat je het geteste Docker-artifact deployt naar je productieomgeving. Dit deploymentproces garandeert consistentie: het exacte artifact dat in STAGING getest is, wordt naar PROD gedeployd. Zo zijn er geen verschillen tussen geteste en gedeployde code.</li>
</ul>
<h2 id="key-takeaways">Belangrijkste punten</h2>
<ul>
<li><strong>Geen branch-merges meer voor deployment</strong>: in deze gestroomlijnde workflow worden STAGING- en PROD-branches niet meer gebruikt om code te mergen. In plaats daarvan verwijzen ze naar specifieke deployment-omgevingen waar Docker-artifacts voor tests en productie gedeployd worden.</li>
<li><strong>Focus op artifacts</strong>: de nadruk ligt op het aanmaken, testen en deployen van Docker-artifacts. Deze artifacts vertegenwoordigen stabiele versies van je applicatie, klaar voor STAGING en uiteindelijk voor PROD.</li>
<li><strong>Consistentie en betrouwbaarheid</strong>: door hetzelfde Docker-artifact van STAGING naar PROD te deployen, weet je zeker dat wat getest is exact is wat er uitgebracht wordt. Dat vergroot de betrouwbaarheid en voorspelbaarheid van je deployments.</li>
</ul>
<p>Deze aanpak vereenvoudigt het deploymentproces, verkleint het risico op fouten en zorgt ervoor dat elke release grondig getest wordt in een omgeving die productie zo nauw mogelijk benadert.</p>
<h2 id="are-multiple-branches-still-needed">Zijn meerdere branches nog nodig?</h2>
<p>In deze nieuwe workflow hebben de STAGING- en PROD-branches in de Git branching strategy geen actieve functie meer en zijn ze dus overbodig geworden. Ze hielpen om bij te houden welke versies van de code naar welke omgeving gedeployd waren. Dat probleem hebben we nu opgelost door een specifieke versie van een artifact te draaien.</p>
<h2 id="conclusion">Conclusie</h2>
<p>Door Docker op te nemen in onze Git branching strategy stroomlijnen we het hele traject van development tot deployment: elke merge naar MAIN triggert een Docker-image met de potentie om naar productie te gaan. De sleutel van deze strategie is het vroeg in de developmentcyclus aanmaken van Docker-artifacts, zodat continuous integration en delivery mogelijk blijven zonder in te leveren op kwaliteit en stabiliteit.</p>
<p>Door de veranderingen aan de Git-branches te begrijpen en het proces van het aanmaken en deployen van Docker-artifacts, kunnen we met vertrouwen omgaan met de complexiteit van CI/CD. Zo leveren we alle nieuwe features en fixes zo efficiënt en betrouwbaar mogelijk af bij de gebruikers.</p>
<h2 id="hotfixing">Hotfixes</h2>
<p>We hebben het nog niet gehad over hotfixes. Zoals ik al zei, verdient dit een eigen post. Je leest hier alles over hotfixes en de valkuilen die daarbij komen kijken.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/"><span class="bookmark__body"><span class="bookmark__title">Hotfixing with Docker Artifacts in a Single Branch Strategy</span><span class="bookmark__description">Master hotfixes in Kubernetes with Docker. Learn to rapidly deploy critical fixes in a CI/CD pipeline focusing on streamlined branching. Perfect for tackling urgent issues without disrupting flow. Dive in now.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/02-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/02-thumb_hu_31f1067c4e9c7ce6.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
    <item>
      <title>Waarom &#34;AI&#34; je verraste</title>
      <link>https://www.allsubjectsmatter.nl/nl/why-ai-caught-you-by-surprise/</link>
      <guid isPermaLink="false">65b0c14894a6360001d8ccaa</guid>
      <pubDate>Tue, 23 Jan 2024 22:58:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Tech]]></category><category>&lt;![CDATA[AI and LM]]></category>
      <description>&lt;![CDATA[Een kijkje in de AI-(r)evolutie. Hoe intentie-begrip AI veranderde, van Flow.ai tot ChatGPT, en de digitale wereld stap voor stap transformeerde. Lees verder voor mijn eigen, unieke inzichten!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/why-ai-caught-you-by-surprise/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Als techliefhebber heb ik veel ontwikkelingen in AI zien langskomen, vooral op het gebied van taalverwerking en interactie met gebruikers. Als ik daarop terugkijk, wil ik al een tijdje delen waarom de recente sprongen in AI volgens mij minder verrassend zijn dan je misschien denkt.</p>
<p>Ik gebruik door elkaar heen verschillende termen voor hetzelfde. Ik spreek over AI en over LLM (Large Language Model) voor de huidige generatie systemen zoals ChatGPT.</p>
<h2 id="my-first-encounters-with-ai">Mijn eerste kennismaking met &ldquo;AI&rdquo;</h2>
<p>Mijn reis met LLM&rsquo;s begon eerder dan veel mensen beseffen. Mijn eerste ervaringen met Flow.ai had ik rond 2018/2019. Toen was mij al duidelijk dat er iets stond te gebeuren. Het grote publiek had echter nauwelijks door wat er in de AI-ontwikkeling broeide. Feit is dat bedrijven als Flow.ai (inmiddels onderdeel van Khoros) de fundering legden voor de AI-systemen van nu, zoals ChatGPT.</p>
<p>Lees anders het stuk dat ik in 2019 schreef over onze ervaringen met Flow.AI, samen met Stefan Samba.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/nl/nl/how-333travel-is-revolutionizing-the-travel-industry/"><span class="bookmark__body"><span class="bookmark__title">2019 - Blast from the past: How 333travel is revolutionizing the Travel industry</span><span class="bookmark__description">Discover how 333travel’s AI ‘Joy’ redefines customer interaction, blending AI’s intent understanding with personal touch. A game-changing journey in AI-led travel solutions!</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/nl/why-ai-caught-you-by-surprise/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/nl/why-ai-caught-you-by-surprise/bookmarks/01-thumb_hu_e0b56fb8f4d4ac9.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<h2 id="flowai-a-missed-opportunity-for-333travel">Flow.ai: een gemiste kans voor 333travel</h2>
<p>Flow.ai was voor mij toen een eyeopener. Het liet me zien wat neural network-gebaseerde conversationele AI in huis had. Als ik er nu op terugkijk, waren we met de integratie van deze technologie bij 333travel in 2018 misschien onze tijd te ver vooruit. Onze natuurlijke manier van mensen helpen met het aanvragen van een voorstel kreeg met een LLM niet de grip die we hoopten. Onze klanten (gelukkig?) wilden liever met menselijke specialisten praten dan met bots. Dat maakte duidelijk dat de technologie veelbelovend was, maar dat de markt er nog niet klaar voor was.</p>
<blockquote class="pull-quote"><p>Mensen vonden het vreselijk om met &ldquo;ouderwetse&rdquo; chatbots te praten die je niet begrepen, door het gebrek aan intent-understanding.</p>
</blockquote>
<h2 id="the-siri-example">Het voorbeeld van Siri</h2>
<p>Laten we het even over Siri hebben, de virtuele assistent van Apple. Sinds de lancering in 2011 heeft Siri wel wat stappen gezet. Maar naar mijn mening zijn die verbeteringen op het gebied van intent learning op z&rsquo;n best bescheiden. Siri, net als tools als Cortana en Google Assistant, had nog steeds hele specifieke commando&rsquo;s nodig en begreep intenties vaak niet. Er is wel wat vooruitgang geboekt, maar de leercurve in het begrijpen van menselijke taal — en vooral intentie — was veel te traag.</p>
<ul>
<li><strong>2010-2011</strong>: In de eerste jaren werd Siri geïntegreerd in de iPhone 4S, met basale spraakherkenning en commando-uitvoering.</li>
<li><strong>2012-2013</strong>: Uitbreiding naar nieuwe talen en dialecten, waardoor de gebruikersbasis groeide.</li>
<li><strong>2014-2015</strong>: Toetreding tot nieuwe markten en de lancering van SiriKit, waarmee apps van derden konden integreren.</li>
<li><strong>2016-2018</strong>: Introductie op macOS en de release van Shortcuts voor gepersonaliseerde spraakcommando&rsquo;s.</li>
<li><strong>2020-2021</strong>: matige verbeteringen in taalvertaling, integratie met het ecosysteem van Apple en betere spraakherkenning.</li>
</ul>
<h2 id="ai-and-customer-interaction">AI en klantinteractie</h2>
<p>Als techliefhebber vind ik ChatGPT op veel vlakken indrukwekkend, maar het is geen vervanging voor de gedetailleerde kennis die menselijke specialisten bieden. Neem nou een touroperator. Een goed reisplan opstellen is een heel ander verhaal. Het laatste wat je wilt, is dat daar fouten in sluipen.</p>
<p>Waarom zou je een AI met de klant laten praten en die daarna vervangen door een mens? Is het omgekeerde niet veel logischer? Laat onze specialisten een AI gebruiken om een persoonlijk reisplan te maken en dat sneller versturen met AI. Je klant bind je in de verkoop nog altijd het beste met echte menselijke interactie.</p>
<blockquote>
<p>Als je niet weet waar je het over hebt, kun je fouten in de antwoorden van een AI ook niet herkennen. Dat blijft altijd een grote zorg. Kennis uit de eerste hand blijft verplicht. Daarom is iets als ChatGPT een tool die gebruikt moet worden door mensen die het onderwerp beheersen, om hun werk te versnellen.</p>
</blockquote>
<p>Ik geloof niet in losse integraties op websites die ruimte innemen en doen alsof ze een slimme AI zijn. De zoekfunctie die al op websites aanwezig is, lost dit probleem op. En mensen die een AI (LLM) gaan gebruiken, willen niet elke keer met een andere LLM praten. Ze zetten hun eigen PA in om die vragen te beantwoorden, als dat nodig is.</p>
<blockquote>
<p>De eerste stap die bedrijven moeten zetten, is een zoekmachine op hun website implementeren die intentie begrijpt. Door intentie te begrijpen kun je mensen een echt nuttig antwoord geven, gebaseerd op de content op je website.</p>
</blockquote>
<p>Je hebt een diepgaande integratie nodig tussen je LLM en je rijke content. Zeker in de reisbranche. Je website slimmer maken met een LLM is volgens mij veel logischer dan een popup-&ldquo;persoon&rdquo; waarmee je kunt chatten.</p>
<blockquote class="pull-quote"><p>De toekomst van AI is ongetwijfeld spannend, maar het is een pad dat we met zowel optimisme als voorzichtigheid moeten bewandelen.</p>
</blockquote>
<h2 id="my-take-on-ais-future">Mijn blik op de toekomst van AI</h2>
<p>Vooruitkijkend zie ik AI als een vakgebied vol mogelijkheden, maar ook vol uitdagingen. De reis van de vroege systemen met vaste commando&rsquo;s naar de intuitieve, contextbewuste modellen van vandaag is fascinerend geweest. Toch is er nog een lange weg te gaan voordat AI mensen echt kan begrijpen en op een dieper niveau met mensen kan omgaan.</p>
<h2 id="conclusion">Conclusie</h2>
<p>De recente ontwikkelingen van AI lijken voor veel mensen plotseling en verrassend, maar ze zijn onderdeel van een lang, doorlopend proces van technologische evolutie. Mijn ervaringen bij 333travel, vooral met innovatieve platforms als Flow.ai, hebben me de potentie én de beperkingen van AI laten zien. Voor de toekomst vind ik het cruciaal om ons enthousiasme voor AI af te wegen tegen een realistisch beeld van wat het nu wel en niet kan.</p>
<blockquote class="pull-quote"><p>Je bent uniek door dingen dicht bij jezelf te houden, niet door technologie je kernactiviteiten te laten overnemen. Houd altijd een persoonlijke touch!</p>
</blockquote>
]]></content:encoded>
    </item>
  </channel>
</rss>
