<?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/</link>
    <description>How Remco Loup, CEO/CTO of 333travel, looks at problems and solves them: with data, with teams, and with whatever technology fits.</description>
    <language>en</language>
    <image><url>https://www.allsubjectsmatter.nl/images/icon.png</url><title>All Subjects Matter</title><link>https://www.allsubjectsmatter.nl/</link></image>
    <lastBuildDate>Wed, 11 Mar 2026 09:54:10 +0100</lastBuildDate>
    <atom:link href="https://www.allsubjectsmatter.nl/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Can an AI conversation be a travel channel? Part 2: The technical architecture</title>
      <link>https://www.allsubjectsmatter.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[Building]]></category>
      <description>&lt;![CDATA[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 more instant.]]></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/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/" data-preview>Part 1</a>, I wrote about why we built an AI travel interface from scratch, what we learned from our first failed attempt in 2019, and why a plugin can never get you to a fully integrated conversational experience. This part is the how. The architecture, the search pipeline, the memory problem that took four attempts to solve, and the UI/UX work that makes 25 seconds feel like 5.</p>
<h2 id="the-architecture">The architecture</h2>
<p>Since this is a tech blog and not a marketing page, let me show you what&rsquo;s actually running. This isn&rsquo;t a weekend project. This is production infrastructure.</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>The frontend is a React application with real-time WebSocket communication. No polling. No request-response cycles where you wait for a full answer. Every token streams in real-time, buffered at 50ms intervals to prevent UI flicker. Product results push to the interface the moment they&rsquo;re found, before the LLM even starts composing its response.</p>
<p>The backend is Python/FastAPI with a modular tool system. The LLM doesn&rsquo;t just generate text. It has access to 9 specialized tools and decides autonomously which ones to use. Need to search trips? It calls <code>search_products</code>. Need destination information? It calls <code>search_knowledge</code>. Need to recall what was discussed earlier? It queries conversation memory semantically.</p>
<blockquote>
<p>The LLM isn&rsquo;t generating answers. It&rsquo;s orchestrating tools and composing a response from real data.</p>
</blockquote>
<p>The LLM layer runs with a multi-provider fallback system. If the primary provider goes down, the system automatically switches to the secondary. No downtime. No errors. The customer never notices.</p>
<p>And there&rsquo;s already an MCP server running, the protocol I described in my Web 4.0 post. External AI assistants can already query our travel data through it. It&rsquo;s early days, but the infrastructure for AI-to-business communication is live. When Lisa&rsquo;s AI assistant wants to search 333travel&rsquo;s products, the endpoint already exists.</p>
<h3 id="the-rag-pipeline-where-the-real-work-happens">The RAG pipeline: where the real work happens</h3>
<p>RAG, Retrieval-Augmented Generation, is the backbone of the entire system. It&rsquo;s what separates &ldquo;GPT with a knowledge base&rdquo; from &ldquo;an AI that actually knows your product.&rdquo; But RAG done poorly is almost worse than no RAG at all.</p>
<p>We maintain three separate vector collections:</p>
<table>
	<thead>
			<tr>
					<th>Collection</th>
					<th>What&rsquo;s in it</th>
					<th>What it does</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Travel Products</td>
					<td>Roundtrips, hotels, tours, cruises</td>
					<td>Powers the main product search</td>
			</tr>
			<tr>
					<td>Travel Details</td>
					<td>Day programs, excursions, inclusions</td>
					<td>Answers specific itinerary questions</td>
			</tr>
			<tr>
					<td>Travel Knowledge</td>
					<td>Blogs, destination guides, travel info</td>
					<td>Provides context and inspiration</td>
			</tr>
	</tbody>
</table>
<p>When a customer asks something, the query gets embedded and thrown against the relevant collection. But here&rsquo;s the critical part: <strong>we don&rsquo;t just take the top vector matches and call it a day.</strong></p>
<p>Vector similarity alone is mediocre. It gets you in the right ballpark, but it doesn&rsquo;t get you the best results. So we do a broad fetch, significantly more results than we need, and then run them through a reranker. The reranker evaluates each query-document pair for actual semantic relevance, not just embedding proximity.</p>
<p>The difference is enormous. It&rsquo;s the difference between &ldquo;here are trips that contain some of the words you used&rdquo; and &ldquo;here are trips that actually match what you&rsquo;re looking for.&rdquo;</p>
<p>But it goes further than that. Before the query even hits the vector database, it runs through a fuzzy normalization layer. Every country and location name in our system is cached at startup. When a customer types &ldquo;Bali&rdquo; we know they mean Indonesia. When they write &ldquo;Tailand&rdquo; we know what they meant. Aliases, spelling variations, language differences, all resolved before the search begins. Without this, you get empty results for queries that should have matched. And on top of that, the search combines semantic similarity with metadata filters. Country, duration, product type, these aren&rsquo;t just fields in a database. They&rsquo;re active filters that work together with the vector search to narrow results before the reranker even starts.</p>
<p>After reranking, the results split into two paths. The full set of reranked results pushes to the frontend immediately, but blurred. The customer sees product cards appearing while Joy is still thinking. It signals progress without overwhelming. Meanwhile, the LLM gets a stripped-down version of those results, only the fields it needs to reason about. This projection saves roughly a third of the tokens, which means faster responses and lower costs. Then Joy composes her answer and discusses the 4 or 5 trips that genuinely fit the question. Only those discussed products become visible to the customer. The rest disappear. What the customer sees is a curated, considered selection. Not a list of 10 search results. A recommendation of 5 that actually make sense.</p>
<blockquote>
<p>Broad fetch → Rerank → Project. Three steps that make the difference between a search result and a recommendation.</p>
</blockquote>
<h3 id="the-memory-problem">The memory problem</h3>
<p>This is the part that took the most iterations to get right.</p>
<p>Conversational AI has a dirty secret: it doesn&rsquo;t remember anything. Every LLM call is stateless. The model has no idea what was said before unless you explicitly feed it the context. And there&rsquo;s a limit to how much context you can feed.</p>
<p>For a simple Q&amp;A chatbot, this doesn&rsquo;t matter. Someone asks a question, gets an answer, done.</p>
<p>But for a travel assistant? Memory is everything.</p>
<p>A customer says: &ldquo;I want to go to Thailand for two weeks.&rdquo; Three turns later: &ldquo;Actually, make it three weeks. And I want to include Laos.&rdquo; Five turns later: &ldquo;What was that trip you showed me earlier with the cooking class?&rdquo;</p>
<p>Without memory, every turn is a blank slate. The system doesn&rsquo;t know about Thailand. Doesn&rsquo;t know about the three-week change. Doesn&rsquo;t know about Laos. Definitely doesn&rsquo;t know about the cooking class trip from seven messages ago.</p>
<blockquote>
<p>Without memory, every message is a first date. Your AI has no idea what happened before.</p>
</blockquote>
<p>This wasn&rsquo;t solved in one attempt. Not in two. Not even in three.</p>
<p>The first version was simple: feed the last 10 question-answer pairs into the LLM context. It worked for short conversations. But our conversations aren&rsquo;t short. Customers explore, compare, come back to earlier ideas, change their mind. Ten turns isn&rsquo;t enough when someone has been chatting for twenty minutes about three different countries.</p>
<p>So we tried generating summaries in the background. The idea was sound: condense the conversation into key points so the LLM always has the full picture without eating up the entire context window. The reality was different. The LLM started hallucinating details into the summaries. Mixing up countries. Attributing preferences the customer never mentioned. The summary was supposed to be the source of truth, and it was making things up.</p>
<p>Then we designed a three-stage memory system: short-term, ephemeral, and long-term. The ephemeral layer was supposed to track which topics belonged to which destination, which preferences applied to which part of the trip. On paper, elegant. In practice, a nightmare. It required the AI to understand complex relational context, like &ldquo;when they said budget was flexible, they meant for Thailand, not for Vietnam.&rdquo; It couldn&rsquo;t. Too many edge cases. Too fragile.</p>
<p>What we landed on was simpler and more robust. Every message gets embedded as a vector in a dedicated memory collection in the same database we use for product search. When the customer says &ldquo;that trip with the cooking class,&rdquo; the system runs a semantic search across all past messages in that session. It finds the relevant message even if the exact words don&rsquo;t match. Maybe Joy called it a &ldquo;culinary experience&rdquo; three turns ago. Vector search doesn&rsquo;t care about word matching. It matches meaning. Combined with the last 10 exchanges in the prompt for immediate context, Joy doesn&rsquo;t need to make that tool call for every question. She has the recent conversation right there, and the full history searchable when she needs it.</p>
<p>Four attempts. Each one taught us something about what AI can and can&rsquo;t do with context. The final version works not because it&rsquo;s the smartest design, but because it&rsquo;s the most honest about the limitations.</p>
<blockquote>
<p>AI memory isn&rsquo;t one problem. It&rsquo;s two: recalling what was said, and understanding what was meant. We needed two different systems to solve them.</p>
</blockquote>
<h3 id="uiux-the-invisible-work">UI/UX: the invisible work</h3>
<p>Here&rsquo;s where most AI projects die, and they don&rsquo;t even realize it.</p>
<p>You can have the most sophisticated RAG pipeline in the world. The smartest retrieval. The best reranking. But if the interface feels clunky, slow, or confusing, none of it matters. The customer doesn&rsquo;t care about your architecture. They care about how it feels.</p>
<blockquote>
<p>If your AI is smart but your interface is slow, you built a genius locked in a closet.</p>
</blockquote>
<p>We went deep on this. And &ldquo;deep&rdquo; means dozens of iterations on things most people would consider details. But details are the product.</p>
<p><strong>Streaming that feels natural.</strong> LLM responses stream token by token over WebSocket. But raw streaming creates flickering text that&rsquo;s jittery and unpleasant. We buffer at 50ms intervals. Fast enough to feel real-time, slow enough to prevent constant re-renders. The difference is subtle but massive in terms of perceived quality.</p>
<p><strong>The speed tradeoff.</strong> Let&rsquo;s be honest about something: a product search with a full answer takes 20 to 30 seconds. That&rsquo;s long. We know it&rsquo;s long. But we made a deliberate choice: quality over speed. The broad fetch, the reranking, the semantic matching, every step in the pipeline exists because it makes the results better. Cut any of them and you get faster responses with worse recommendations. We&rsquo;d rather have Joy work for 25 seconds and deliver 5 trips that genuinely fit than return 10 mediocre matches in 8 seconds.</p>
<p>So the question became: how do we make 25 seconds not feel like 25 seconds? That&rsquo;s where the UX work came in. Joy shows what she&rsquo;s doing. Searching products. Looking up destination information. Checking conversation history. Every tool call is visible as it happens. Product cards push in blurred while she&rsquo;s still composing her answer. Once the answer is ready, only the trips Joy actually discusses become visible. The rest fade away. The customer never stares at a blank screen. They see the process unfold. And it turns out that watching something work feels completely different from waiting for something to finish.</p>
<p>Part of the latency is also a language problem. Dutch is poorly represented in most embedding and language models. The semantic understanding that works beautifully in English degrades when your customers and your product data are in Dutch. It means more processing, more careful matching, more effort to get the same level of quality.</p>
<p><strong>What is GPT-NL?</strong> <a href="https://gpt-nl.nl/" data-external>GPT-NL</a> is a Dutch initiative to build a large language model specifically trained on and optimized for the Dutch language.</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/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/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>Where current models like GPT and Claude treat Dutch as a non existent language, GPT-NL aims to make Dutch a first-class citizen in AI. For applications like ours, where both the customer and the product data are Dutch, a model that truly understands the language could significantly improve semantic search quality and reduce the processing overhead we currently need to compensate for that gap. We&rsquo;re watching this project closely.</p>
<p>And then there&rsquo;s model selection. The quality of the conversation depends heavily on which LLM you use. We run on Sonnet 4.6 as our primary, and the difference with smaller or cheaper models is not subtle. It&rsquo;s the difference between a conversation that feels natural and one that feels scripted. Haiku, the faster and cheaper option, feels like a downgrade the moment you switch. GPT serves as our automatic fallback, but it&rsquo;s a step back in conversational quality. These aren&rsquo;t interchangeable commodities. The model you choose defines the experience your customer gets.</p>
<p><strong>Instant page loads.</strong> The entire conversation persists in local storage. When you refresh the page or come back later, your conversation is already there. Instantly. No loading spinner. No &ldquo;reconnecting&hellip;&rdquo; message. The sync with the server happens silently in the background.</p>
<p><strong>Markdown normalization.</strong> LLMs have opinions about formatting. Claude loves bold headers with emoji. GPT loves numbered lists. We normalize everything server-side. Bold-with-emoji gets converted to clean headers. Horizontal rules get stripped. Font weights get tuned down. The result looks like it was designed, not generated. The customer should never feel like they&rsquo;re reading AI output.</p>
<p><strong>Session continuity.</strong> Your session survives across page refreshes, browser closes, and reconnections. Come back a day later, your conversation is still there. Your saved trips are still there. And if a session does expire, you get a clean overlay explaining what happened, not a broken page or a cryptic error.</p>
<p><strong>The lead flow.</strong> When a customer is ready to talk to a specialist, the system generates a travel proposal pre-filled with what it already knows from the conversation. Destinations discussed, trip preferences, products explored. Minimal friction. Maximum context. The human specialist picks up exactly where Joy left off. That&rsquo;s not a handoff. That&rsquo;s the whole point. Joy exists to make that human conversation better, not to avoid it.</p>
<blockquote>
<p>Every one of these details took multiple iterations. Every one of them is invisible to the customer. That&rsquo;s the point.</p>
</blockquote>
<h3 id="security-an-honest-note">Security: an honest note</h3>
<p>I&rsquo;ll keep this section brief and honest. Security isn&rsquo;t something you finish. It&rsquo;s something you work on constantly.</p>
<p>We have a dual guard system. An input guard before the LLM that catches prompt injection attempts, and an output guard after the LLM that catches credential leaks or system prompt fragments in responses. Both are deterministic and add virtually zero latency.</p>
<p>The system prompt is hardened with anti-jailbreak instructions. There&rsquo;s rate limiting, bot protection, and IP management for persistent abuse.</p>
<p>Is it bulletproof? No. Nothing is. But it&rsquo;s built with defense in depth in mind, and we iterate on it as new attack patterns emerge. If you&rsquo;re building any AI interface that talks to the public and you haven&rsquo;t thought about input/output guards, you have a problem waiting to happen.</p>
<h3 id="the-bigger-picture-an-extra-channel-not-a-replacement">The bigger picture: an extra channel, not a replacement</h3>
<p>Let me be very clear about what Joy is and what she isn&rsquo;t.</p>
<p>Joy is not a replacement for our website. She&rsquo;s not a replacement for phone or email. She&rsquo;s an additional channel. A new way for customers to explore our products, alongside everything that already exists. She will never be a replacement for human contact. We value that the most in our company. Joy is a means to human contact. She helps customers figure out what they want, so that when they talk to one of our specialists, that conversation starts at a completely different level.</p>
<blockquote>
<p>Joy isn&rsquo;t the destination. She&rsquo;s the road that leads to a real conversation with a real person.</p>
</blockquote>
<p>The MCP server is already live, which means AI assistants can already query our data programmatically. When the Web 4.0 vision materializes, when Lisa&rsquo;s AI assistant autonomously searches for her perfect Thailand trip, the endpoint already exists. Not because we scrambled to build it. Because we&rsquo;ve been exploring this infrastructure for over a year.</p>
<p>Joy isn&rsquo;t the end product. She&rsquo;s the beginning of a channel strategy that includes both human-to-AI and AI-to-AI interactions. And she&rsquo;s a test, a very serious, very thorough test, of whether conversational AI can actually add value for travel customers today.</p>
<p>The results so far have been better than I expected. Not flawless, nothing is, but genuinely impressive in how natural the conversations feel and how relevant the recommendations are. When you see a customer describe a vague idea and Joy comes back with trips that actually fit, you realize this isn&rsquo;t a gimmick. It works. And it works because of everything underneath it.</p>
<h3 id="so-what-are-we-trying-to-solve-again">So, what are we trying to solve again?</h3>
<p>I opened with this question because it&rsquo;s the one that guided everything we built. Not &ldquo;how do we use AI?&rdquo; Not &ldquo;what&rsquo;s our competitors doing?&rdquo; Not &ldquo;how do we look innovative?&rdquo;</p>
<p>Just: what problem are we solving, and does this solution actually work?</p>
<p>We built Joy to find out. With real architecture. Real data. Real customers. And the willingness to say &ldquo;it&rsquo;s not ready yet&rdquo; if that turned out to be the case.</p>
<p>If you&rsquo;re thinking about adding AI to your product, start there. Not with the tool. Not with the vendor pitch. Not with the pressure to keep up.</p>
<p><strong>Start with the question. Build from the answer.</strong></p>
<p>Your customers deserve better than a FAQ with a face. They deserve a real conversation. And eventually, a real person. Make sure your AI leads them there, not away from it.</p>
<p>If you haven&rsquo;t read <a href="https://www.allsubjectsmatter.nl/can-a-conversation-be-a-travel-channel-we-built-the-architecture-to-find-out/" data-preview>Part 1</a>, that&rsquo;s where the story starts: why we built this, what we learned from failing at it in 2019, and why you can&rsquo;t get here with a plugin.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.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/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/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>Can an AI conversation be a travel channel? Part 1: We built one to find out.</title>
      <link>https://www.allsubjectsmatter.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[Building]]></category><category>&lt;![CDATA[Strategy]]></category>
      <description>&lt;![CDATA[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.]]></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 built an AI-powered travel interface from scratch. Not a chatbot. Not a widget. Not a GPT-wrapper with a smiley face bolted onto a website. A full conversational interface with its own vector database, its own retrieval pipeline, its own memory system, and real-time streaming over WebSockets. Not to replace anything. To test whether conversational AI is actually ready to be a meaningful channel for travel. Here&rsquo;s what we found, what it took, and why we think most of what&rsquo;s out there right now is noise.</p>
<hr>
<p>This is a two-part story. This first part covers the why: what problem we&rsquo;re solving, what we learned from failing at this before, and why a plugin will never get you here. <a href="https://www.allsubjectsmatter.nl/the-technical-architecture-behind-a-conversational-travel-channel/" data-preview>Part 2</a> goes deeper on the architecture: the RAG pipeline, the memory system, the UI/UX decisions, and the technical tradeoffs we made.</p>
<h2 id="what-are-you-actually-trying-to-solve">What are you actually trying to solve?</h2>
<p>That&rsquo;s the question I keep coming back to. Every time I see a company announce their new &ldquo;AI-powered&rdquo; feature. Every time someone asks me what we&rsquo;re doing with AI. Every time a vendor pitches yet another plugin.</p>
<p>It&rsquo;s the most important question in tech right now. And almost nobody is asking it.</p>
<p>Instead, the entire industry is working backwards: starting with &ldquo;we need to do something with AI&rdquo; and then looking for a place to put it. Companies hear &ldquo;AI&rdquo; and they hear &ldquo;innovation&rdquo; and they think they&rsquo;re falling behind if they don&rsquo;t do something right now.</p>
<blockquote>
<p>The pressure to &ldquo;do something with AI&rdquo; has become louder than the question of what are you trying to achieve.</p>
</blockquote>
<p>And so what happens? Companies reach for the most visible, most marketable application they can find: a chatbot. Slap it on the website. Give it a name and a friendly face. Connect it to a knowledge base. Done. &ldquo;We do AI now.&rdquo;</p>
<p>Here&rsquo;s the problem: the integration is always limited. Some connect to a product database. Some pull live availability. But as a plugin, there&rsquo;s a ceiling to how deep that integration goes. It&rsquo;s always working with whatever data it gets access to, in whatever format the API provides, with whatever context the vendor decided was enough. The result is an interaction that looks conversational on the surface but is fundamentally constrained in what it can actually do.</p>
<p>And because it&rsquo;s a plugin, it can never be fully seamless. It will always be a separate layer on top of your website, not part of it. A different design language. A different interaction model. A different experience. The customer feels it, even if they can&rsquo;t articulate it. It&rsquo;s the difference between a room that was designed as a whole and a room where someone placed a kiosk in the corner.</p>
<blockquote>
<p>A smarter gatekeeper is still a gatekeeper. Your customers wanted a door.</p>
</blockquote>
<p>We&rsquo;ve been down this road ourselves. Back in 2019, we built the <a href="https://www.allsubjectsmatter.nl/how-333travel-is-revolutionizing-the-travel-industry/" data-preview>first version of Joy</a> on Flow.ai. An intent-based chatbot that could understand natural language, extract travel preferences from a single sentence, and generate leads for our specialists. It worked. Technically. But customer engagement was too low to justify keeping it alive. We retired Joy in early 2022.</p>
<p>The lesson wasn&rsquo;t that the technology was bad. The lesson was that people have learned to distrust chatbots. And rightfully so. For years, companies have used chatbots as a barrier to contact, even when they claim otherwise. Customers know this. They&rsquo;ve experienced it. After three wrong answers, you don&rsquo;t think &ldquo;let me try again.&rdquo; You think &ldquo;how do I get to a real person?&rdquo; Some people don&rsquo;t even bother engaging. They see a chat bubble with a face and their immediate reaction is to close it. Not because they&rsquo;re impatient, but because experience taught them it&rsquo;s a waste of time.</p>
<p>That&rsquo;s not a technology problem. That&rsquo;s a trust problem. And it&rsquo;s one you can&rsquo;t solve by making the same format smarter.</p>
<p>On top of that, the interface of a chatbot plugin is simply limiting. A small window in the corner of your screen. A few lines of text. Maybe a button or two. You can&rsquo;t show rich product cards in that. You can&rsquo;t display a map of a trip. You can&rsquo;t let someone browse, compare, save trips for later. The format dictates the experience, and the format is a box.</p>
<p>That experience is exactly why we didn&rsquo;t build another chatbot. Not a smarter one. Not a better one. Something fundamentally different. A full conversational interface, built from scratch, designed to inspire. Where someone can explore destinations, discover trips they didn&rsquo;t know existed, and when they&rsquo;re ready, transition into contact with a specialist after a conversation that actually felt good. Not after three failed attempts to get past a gatekeeper.</p>
<p>The real question was never &ldquo;how do we add AI to our website?&rdquo; The real question is: where does AI genuinely create value that wasn&rsquo;t possible before? That&rsquo;s what we set out to test.</p>
<h2 id="why-i-built-what-i-built">Why I built what I built</h2>
<p>I didn&rsquo;t build this because I think websites are dead or because I wanted to say we &ldquo;do something with AI.&rdquo; I&rsquo;ve written before about <a href="https://www.allsubjectsmatter.nl/lets-stop-talking-about-ai-and-start-solving-real-problems/" data-preview>how much I despise that mentality</a>.</p>
<p>I built this because of a question I&rsquo;ve been testing for over a year now: is conversational AI truly ready to be a real channel?</p>
<p>In my <a href="https://www.allsubjectsmatter.nl/web-4-0-and-the-autonomous-web-the-rise-of-ai-driven-customers/" data-preview>Web 4.0 post</a>, I wrote about the autonomous web, a world where AI assistants don&rsquo;t just retrieve information but act as independent digital consumers on behalf of their humans. Where Lisa tells her AI assistant &ldquo;find me a two-week trip to Thailand&rdquo; and the AI contacts travel companies through MCP-enabled interfaces, negotiates, refines, and presents options. Without Lisa ever opening a browser.</p>
<p>That might sound like removing humans from the equation. It&rsquo;s not. The technology enables full autonomy. But that doesn&rsquo;t mean we want to remove the human. It means the customer gets to choose. And our default will always be: lead to a person. More on that later.</p>
<p>That future is coming. Maybe not tomorrow, but it&rsquo;s coming. And if you&rsquo;re a travel company that takes itself seriously, you&rsquo;d better be ready for it.</p>
<p>So I built Joy, an AI travel assistant for 333travel, not as a product launch, not as a marketing play, but as a serious test. Can we build a conversational interface that actually adds value? That understands our products? That doesn&rsquo;t hallucinate as much? That creates a genuinely useful experience as an additional channel next to our website, phone, and email?</p>
<blockquote>
<p>I didn&rsquo;t build this to replace our website. I built this to answer a question: can a conversation be a meaningful channel for personalized travel?</p>
</blockquote>
<h2 id="what-this-actually-is">What this actually is</h2>
<p>Let me be specific about what Joy does:</p>
<p>Joy sits on top of our entire product database. Every roundtrip, every hotel, every excursion, every destination, all embedded as vectors in a dedicated database. When a customer says &ldquo;I want something adventurous in Southeast Asia, around two weeks, not too touristy,&rdquo; Joy doesn&rsquo;t keyword-match against a PDF. She searches semantically across hundreds of products, reranks results based on actual relevance, and responds with options that genuinely fit the intent.</p>
<p>And she remembers the conversation. Not just the last message, the whole thread. She knows what trips were discussed, what was saved, what was dismissed. When you say &ldquo;that trip you mentioned earlier with the cooking class,&rdquo; she understands what you mean even if she originally called it a &ldquo;culinary experience.&rdquo;</p>
<p>But here&rsquo;s the key difference with any chatbot out there: she doesn&rsquo;t just know travel in general. She knows <em>our</em> travel. Our specific products. Our specific itineraries. Our destinations that our team personally visited. That&rsquo;s not something you can easily bolt on with a plugin, no matter how smart the LLM behind it is.</p>
<p>And the moment you want human contact? Joy hands the full context to a real travel specialist who knows exactly what you&rsquo;ve been exploring. No &ldquo;please describe your inquiry again.&rdquo; No cold transfer. Full continuity.</p>
<blockquote>
<p>Joy doesn&rsquo;t replace conversations with specialists. She makes them better.</p>
</blockquote>
<p>What she doesn&rsquo;t do (yet): she doesn&rsquo;t handle customer service queries, airline information, or post-booking support. This is purely a product discovery and inspiration channel. We&rsquo;ll get there. But we&rsquo;re not going to pretend she does things she doesn&rsquo;t. And honestly, when someone reaches out to customer service, there&rsquo;s already a problem. That&rsquo;s the worst possible moment to put a machine between you and your customer. That&rsquo;s when you show up. Personally. So we are still debating about what we want her to do there. The nuance is always in the details.</p>
<h2 id="why-you-cant-get-here-with-a-plugin">Why you can&rsquo;t get here with a plugin</h2>
<p>You cannot achieve what we built by installing a plugin. Not with ChatGPT. Not with any off-the-shelf &ldquo;AI widget.&rdquo; Not with any SaaS tool that promises to &ldquo;make your website smarter in minutes.&rdquo;</p>
<p>And it&rsquo;s not because the AI models aren&rsquo;t good enough. The models are fine. The problem is everything around them.</p>
<p>The entire system depends on data that we own, structure, and control. Every trip in our database has been visited by our own team. Every itinerary is our own creation. Every piece of destination knowledge lives in our own systems. This isn&rsquo;t data we license from a third party or pull from a brochure catalog.</p>
<p>That means we can embed it. We can structure it exactly how we need it for semantic search. We can update it the moment something changes. We control the full pipeline from raw product data to vector embedding to retrieval to response.</p>
<blockquote>
<p>You can&rsquo;t build a smart interface on top of piles of dumb data.</p>
</blockquote>
<p>If your product data lives in someone else&rsquo;s system, if you&rsquo;re reselling trips from a catalog, if your &ldquo;database&rdquo; is a collection of PDFs from tour operators, you&rsquo;re at the mercy of whatever structure someone else decided on. You can&rsquo;t build intelligence on top of data you don&rsquo;t understand or control.</p>
<p>And many companies aren&rsquo;t just starting from zero, they&rsquo;re starting from a deficit. Their existing systems are already inadequate. The booking engine is outdated. The product data is scattered across spreadsheets and PDFs. The customer journey has gaps everywhere. And instead of fixing those fundamentals, they think it is smart to layer an AI chatbot on top of it. As if intelligence on top of dysfunction creates something functional. It doesn&rsquo;t. It just makes the dysfunction harder to diagnose.</p>
<blockquote>
<p>You&rsquo;re not solving a problem. You&rsquo;re decorating one.</p>
</blockquote>
<p>Building what we built required three things:</p>
<p><strong>Owning our data.</strong> Every trip is ours. Visited by our team. Structured in our database. Embedded in our vector store. You can&rsquo;t build semantic search over data you don&rsquo;t structure yourself.</p>
<p><strong>Owning our systems.</strong> Our own backend, frontend, deployment pipeline. When we needed a dual reranker, we built it. When we needed semantic conversation memory, we built it. When we needed an MCP server for Web 4.0 readiness, we built it. No vendor approval needed. No feature requests. No roadmap dependencies.</p>
<p><strong>Owning the entire chain.</strong> From the moment a customer opens the chat to the moment they&rsquo;re talking to a specialist, every step is ours. The AI knows our products because we made the products. The specialist knows the conversation because our system feeds it to them.</p>
<blockquote>
<p>The AI is the last 10% of the work. The first 90% is building a company that can support it.</p>
</blockquote>
<p>In <a href="https://www.allsubjectsmatter.nl/the-technical-architecture-behind-a-conversational-travel-channel/" data-preview>Part 2</a>, I&rsquo;ll show you how it is actually running under the hood. The architecture, the search pipeline, the memory system that took many attempts to get right, and why 25 seconds of waiting can feel like 5 when you design it right.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.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/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/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>I Vibecoded an app and this is what I learned.</title>
      <link>https://www.allsubjectsmatter.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[Building]]></category>
      <description>&lt;![CDATA[I built a &#34;production&#34; app using Vibecoding. 97,089 lines generated. Only 25,056 made it to production. Four complete refactors. The AI didn&#39;t fail because of bad prompts. It failed because AI forgets, loses context, and creates duplicate logic no matter how well you guide it.]]></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>I built a &ldquo;production&rdquo; app using Vibecoding. 97,089 lines generated. Only 25,056 made it to production. Four complete refactors. The AI didn&rsquo;t fail because of bad prompts. It failed because AI forgets, loses context, and creates duplicate logic no matter how well you guide it. Vibecoding isn&rsquo;t about letting AI build for you. It&rsquo;s about knowing your system well enough to filter the noise. Without that knowledge, you&rsquo;re only shipping guesses.</p>
<hr>
<h2 id="what-vibecoding-actually-means"><strong>What Vibecoding actually means</strong></h2>
<p>People think vibecoding means letting AI build your app while you watch. Reality check kicking in here, this is not at all the case if you need an actual working product with the expected result every time.</p>
<p>Vibecoding still means you take control. You prompt with precision. You experiment. You guide. You filter. It&rsquo;s still hands-on work, just with different leverage.</p>
<blockquote class="pull-quote"><p>&ldquo;A fool with a tool is still a fool&rdquo;</p>
</blockquote>
<p>And if you don&rsquo;t know what you&rsquo;re doing? The result is the same as it always was: a mess. Vibecoding without the right knowledge is just plain wrong. And the main problem with AI, is that it keeps producing code after code after code. It just does what you ask. it isn&rsquo;t critical about that. So if you don&rsquo;t know what you are doing you are contradicting yourself all the time with programmed functionality that never works together within your app.</p>
<p>Ai is king in fallbacks. I would have had 5 different fallbacks for every api call that fails. Shouldn&rsquo;t we just make the right way work instead of making fallbacks. AI is also lazy, it rather suppresses notices then fixing them. especially if it won&rsquo;t work on the first try.</p>
<h2 id="i-built-a-production-app-with-ai-but-not-because-of-ai"><strong>I built a production app with AI (but not because of AI)</strong></h2>
<p>I used Windsurf to build an entire application. No human developers. No code written by me directly. we use Sonnet 4.5 as our knowledge.</p>
<p>The result after a lot of frustration: a full production game with backend logic, login flows, async data writes, mail queues, admin security, monitoring, and cartesian algorithms that update based on user interaction.</p>
<p>The AI wrote all of it. I haven&rsquo;t touched a single line of code. So even the simples css change. I let AI do it.</p>
<p>But here&rsquo;s what nobody tells often: I had to completely refactor the entire project four times. And not because I prompted poorly. I used KISS principles, DRY standards, PSR guidelines. We wrote decisions and created implementation documents on forehand. We used guidelines for the languages that we used.</p>
<p>It didn&rsquo;t matter in the end.</p>
<p>AI forgets. It loses context. It starts creating duplicate functions without realizing it already wrote that logic three files ago. The more complex your project gets, the more it drifts. And the more it isn&rsquo;t done right in the first attempt AI thinks it needs to maintain backwards compatibility, complicating your code even more. No matter how you prompt it the first time or with project rules.</p>
<blockquote>
<p>Four complete refactors later, I learned: AI doesn&rsquo;t forget context. It never had it ;).</p>
</blockquote>
<p>The only reason the final version works is because I knew exactly what I was building. I had the system in my head. I could spot when AI was repeating itself. I knew when to stop it, when to redirect it, when to start over. I checked every file for duplicate fallback data. duplicates and so on. I also used the very usefull npm package knip</p>
<p><a href="https://www.npmjs.com/package/knip" data-external>https://www.npmjs.com/package/knip</a></p>
<p>You can&rsquo;t vibecode something into production without understanding your product. Period. And you need to accept that bug fixing is relying on your AI to fix it without breaking anything else, Good luck with that&hellip;</p>
<h2 id="the-data-tells-the-rest-of-our-story"><strong>The data tells the rest of our story</strong></h2>
<p>During our Vibecoding, Windsurf produced:</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/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/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 lines of code generated</li>
<li>2,925 prompts sent</li>
<li>5,693 credits used</li>
<li>15,734 tool calls (including 7,000 code edits)</li>
</ul>
<p>What actually made it into production:</p>
<ul>
<li>25,056 lines of code</li>
</ul>
<p>Over 70,000 lines got scrapped, replaced or refactored.</p>
<p>Now, you might think this means you just need to &ldquo;guide AI better&rdquo; or &ldquo;prompt more precisely.&rdquo; That&rsquo;s not the point I wanted to make.</p>
<p>The point is: <strong>you can&rsquo;t prevent AI spew right now.</strong> No matter how well you prompt. No matter how structured your approach is. AI will overproduce. It will generate redundant code. It will lose track of what it already built. It will create noise. AI is build to always post a form of AI Verbal Diarrhea.</p>
<p>That 70k+ lines of wasted output? That&rsquo;s not a bug. That&rsquo;s the current state of Vibecoding.</p>
<p>And if you don&rsquo;t know your system well enough to recognize what&rsquo;s garbage and what&rsquo;s not, you&rsquo;ll ship all of it and never be able to debug even with AI. Or worse, you&rsquo;ll spend personally weeks debugging a codebase you don&rsquo;t understand.</p>
<p>The costs per 1000 credits are around 35 dollar. The development of this game cost me 210 dollar. Every company delivering Large Language Models is losing money. the code I produced should easily have costed 210 x 10 =2.100 dollar at least. I even wonder if they then even made money then.</p>
<p>Apart from the money, is this really worth our precious electricity? I am wondering if you pay the real price, how feasible this will be.</p>
<h2 id="what-people-get-wrong"><strong>What people get wrong</strong></h2>
<p>AI isn&rsquo;t magic. It&rsquo;s a &ldquo;smart&rdquo;, fast assistant. But it doesn&rsquo;t understand what matters to your business. It doesn&rsquo;t grasp edge cases, constraints, risks, performance implications, or security vulnerabilities.</p>
<p>That&rsquo;s your job!</p>
<p>In my case, AI helped build something real. But the AI didn&rsquo;t secure the admin panel. It didn&rsquo;t design the mail queue logic. It didn&rsquo;t decide when to debounce async writes. I did. By being critical and asking the right questions due to build experience in the past.</p>
<p>Without that direction, the entire thing would have collapsed.</p>
<p>And I see this pattern everywhere. Teams building AI products without ownership or direction, just to say they&rsquo;ve &ldquo;done something with AI.&rdquo; It&rsquo;s shallow. It&rsquo;s fragile. It doesn&rsquo;t scale.</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>How many times don&rsquo;t you hear from higher hand: We need to do something with AI. We are falling behind. Behind of what?</p>
<h2 id="the-customer-service-trap"><strong>The customer service trap</strong></h2>
<p>Then there&rsquo;s also the AI customer service hype. Companies claim it&rsquo;s perfect for 24/7 support, faster responses, fewer humans needed.</p>
<p>But if you need a bot to talk to your customers, your real problem is probably somewhere else. AI doesn&rsquo;t fix broken processes. It hides them. Temporarily.</p>
<p>If you think less human contact is a win, you&rsquo;ve already lost. If replying to questions takes a long time you might have to look at other things why people have so many questions and improve on that first.</p>
<h2 id="the-actual-point"><strong>The actual point</strong></h2>
<p>Vibecoding is just another tool. That&rsquo;s all it is. A very cool tool in the right hands tho, but still a tool. And mainly good for prototyping, but with a good dosis of knowledge you can maybe produce standalone apps that actually work, and are at least testable by you and the outcome is traceable to the code written in a transparant way.</p>
<p>It doesn&rsquo;t replace thinking. It should amplify it, and it shouldn&rsquo;t make you think less.</p>
<p>Used right, it can be powerful. But you still need to know what you want to build. You need to own the structure. You need to filter the noise. You need to recognize AI spew when you see it. You need to lead the process.</p>
<p>Otherwise, you&rsquo;re not building a prototype. You&rsquo;re just generating guesses.</p>
<p>The future isn&rsquo;t AI-first. The companies that matter stay human-first. They understand the problem before picking the tool. And they keep asking the only question that counts before reaching out for AI to help them:</p>
<p><strong>What are we actually trying to solve?</strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Let’s Stop Talking About AI (and Start Solving Real Problems)</title>
      <link>https://www.allsubjectsmatter.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[Strategy]]></category>
      <description>&lt;![CDATA[This article explores why AI should be treated as a tool, not a strategy; why hallucinations are mathematically inevitable in current generative models; and why true progress starts with solid architecture, ownership, and clear thinking and not with chasing hype.]]></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 here to stay, that’s not the topic here.</p>
<p>The problem is that <strong>everyone’s talking about AI</strong>, but few are actually thinking clearly about how to really use it. Somewhere along the way, “we use AI” became a marketing tagline, not a technical decision. Everyone wants to be part of the AI wave otherwise, you don’t belong. If you have to believe big tech the way they are forcing it down everyone&rsquo;s throat.</p>
<p>But when you take a small step back, you’ll notice something strange:</p>
<blockquote>
<p>Companies are solving problems with AI that they shouldn’t have in the first place.</p>
</blockquote>
<p>Poor system designs, missing ownership, and spaghetti infrastructures are now being &ldquo;fixed&rdquo; by throwing LLMs on top of this pile of shit. And as a result, systems are becoming even harder to maintain, more expensive, and harder to reason about.</p>
<p>This blog isn’t about tearing down AI.<br>
It’s about <strong>clear thinking, technical ownership</strong>, and using the right tools for the job.</p>
<h2 id="openai-investigation-research-paper">OpenAI investigation research paper</h2>
<p>Recent research by OpenAI and Georgia Tech highlights a core truth that often gets ignored: <strong>AI hallucinations, those moments where models make things up aren’t bugs. They’re a mathematical consequence of how these systems are built.</strong></p>
<p>Even with perfect training data, the current architecture of generative AI rewards confident answers over admitting uncertainty. That means hallucinations are not only common, they’re <em>inevitable</em>, unless we fundamentally change how we train and evaluate these models.</p>
<blockquote>
<p>As the researchers put it: “Hallucinations are mathematically inevitable.” (<a href="https://arxiv.org/pdf/2509.04664" data-external>source</a>)</p>
</blockquote>
<h2 id="how-people-abuse-ai-to-solve-the-wrong-problems">How people abuse AI to solve the wrong problems.</h2>
<p>Let’s take a step back.</p>
<p>If you’re paying $1,200/month for GPT-4 to parse email text, extract phone numbers, reformat JSON, and uppercase strings… you’re not doing innovation. You’re outsourcing common sense to an expensive black box.</p>
<p>Yes, someone recently did that, and realized the cost effect too late. They then moved to GPT-4o-mini and brought the bill down to $200&hellip;. But even then it still is a shit solution:</p>
<blockquote>
<p>Those tasks didn’t need AI at all. Just basic software skills.</p>
</blockquote>
<p>This isn’t an isolated case. It’s happening to much sadly.</p>
<p>People assume AI is their fix to everything, while it only shows the persons incompetence because:</p>
<ul>
<li>It increases costs</li>
<li>Obscures what&rsquo;s actually happening</li>
<li>Introduces hidden dependencies</li>
<li>Makes systems almost impossible to debug and maintain</li>
</ul>
<p>AI is used to <em>cover up</em> a lack of system design, not to improve it.</p>
<h2 id="ai-as-a-natural-language-interface-powerful-but-misused">AI as a Natural Language interface, powerful but misused</h2>
<p>But let’s be fair: LLMs truely are powerful when used right.<br>
They allow you to describe what you want in natural language, and get a result, without understanding the underlying logic.</p>
<p>This makes AI a fantastic tool for:</p>
<ul>
<li>Prototyping</li>
<li>Interacting with large knowledge bases</li>
<li>Summarizing unstructured data</li>
<li>Performing fuzzy matching at scale</li>
</ul>
<blockquote>
<p>LLMs are essentially a new kind of interface, just like the command line, the GUI, or APIs were before.</p>
</blockquote>
<p>But they’re not <em>magic</em>. And they are definitely not for free. And they’re almost always not the best way to solve a problem.</p>
<p>The more critical the function, the more important it becomes to understand:</p>
<ul>
<li>What is being executed?</li>
<li>Where is the data flowing?</li>
<li>Who owns the outputs?</li>
<li>What are the real costs?</li>
</ul>
<h2 id="conversational-ai-the-modern-chatbot-but-still-a-chatbot">Conversational AI, the modern chatbot, but still a chatbot</h2>
<p>There <em>is</em> a path forward for conversational AI. It’s just not the revolutionary leap people think it is with self thinking systems that wont hallucinate and always provide the right answer.</p>
<p>Yes, LLMs can provide smarter, more human-like responses then before LLM&rsquo;s became mainstream.<br>
Yes, they can automate support flows that were previously a lot more complex to script.</p>
<p>But let’s not fool ourselves:</p>
<blockquote>
<p>People still want <strong>real, human contact</strong> with your business especially when something goes wrong.</p>
</blockquote>
<p>If you outsource your entire customer service to an AI assistant, you&rsquo;re sending a clear message (but the wrong one in my opinion) :</p>
<ul>
<li>&ldquo;We don’t care enough to talk to you ourselves.&rdquo;</li>
<li>&ldquo;You&rsquo;re not worth the time of a real person.&rdquo;</li>
<li>&ldquo;We only sell our product. dont come complaining to us please.&rdquo;</li>
</ul>
<p>This probably saves costs in the short term.<br>
But long-term? You’re destroying the trust with your brand. Especially because people typically contact a customer service when:</p>
<ul>
<li>They’re frustrated</li>
<li>They don’t understand something</li>
<li>They have a complaint</li>
</ul>
<p>That is <strong>not</strong> the time to offer them a chatbot, no matter how “smart” it sounds.</p>
<blockquote>
<p>Great customer service is a relationship, not a cost center.</p>
</blockquote>
<p>So yes, use conversational AI to assist your teams. But don’t let it replace human contact where empathy is needed. Dont make AI the interface of talking with your company for customers if you care about your product.</p>
<h2 id="what-you-should-be-using-ai-for">What you should be using AI for</h2>
<p>Let’s flip it around in this section. What <em>is</em> a good use case for AI?</p>
<p>You should use AI when:</p>
<ul>
<li>It helps you do things that were previously too expensive, too slow, or technically too complex</li>
<li>You’ve already optimized your architecture, but still have edge cases worth automating</li>
<li>You&rsquo;re enriching processes, not hiding technical debt</li>
</ul>
<p>For example:</p>
<ul>
<li>Prototyping difficult functionalities so you can test outcomes quicker</li>
<li>Enhancing product search with semantic understanding</li>
<li>If you can make the hallucination visible in data they are perfect for BI (business intelligence)</li>
<li>Generating tailored summaries for large internal reports</li>
<li>Transcribing phone calls and make summaries</li>
<li>Image to text so you can better semantic search on images in large image databases.</li>
</ul>
<p>These are tasks where AI really shines, because they were even more expensive, hard or impossible to solve in traditional systems.</p>
<p>But again: <strong>always weigh the costs</strong>.<br>
Not just in dollars, but in <strong>clarity</strong>, <strong>control</strong>, and <strong>customer experience</strong>. Always try to solve it without using AI first.</p>
<h2 id="tech-maturity-will-determine-who-wins">Tech maturity will determine who wins</h2>
<p>Here’s the core of it all:</p>
<blockquote>
<p>Companies with modern, understandable, well-documented infrastructure and data will win.<br>
Those who keep layering AI on top of their chaos… won’t.</p>
</blockquote>
<p>It’s not about how early you adopted AI. It’s about:</p>
<ul>
<li>Whether your stack is maintainable</li>
<li>Whether your teams really understands it</li>
<li>Whether you can reason about cost, performance, and failure</li>
<li>Whether you own your data, your architecture, and your roadmap</li>
</ul>
<p>The companies that treat AI as a new algorithm/interface to solve problems they could not solve before, will get the most long-term value in my opinion. Don&rsquo;t use AI for it&rsquo;s marketing value big tech wants you to.</p>
<h2 id="offloading-data-ownership-is-a-strategic-risk">Offloading data ownership is a strategic risk</h2>
<p>Let me end with another real example I recently came across.</p>
<p>A company in the travel sector built an app for end-users to access their trip details. They are reselling this to other travel agencies and tour operators. That sounds feasible looking at the bad shape travel is in regarding IT.</p>
<p>So the travelagency can offer the app with all vouchers and itineraries for the trip of their customer. Nothing weird so far except you are offloading all your documents to a third party.</p>
<p>So now this itinerary app provider has access to a lot of your crucial business information. And now the customer buys extra services (e.g. insurance, transfers, upgrades) in the app. You should think this is upsell for the travel agency owning the customer.</p>
<p>Here comes the culprit. The platform suddenly gives <strong>commission</strong> to the touroperator&hellip; So is the app provider now also providing extra services to your customer??</p>
<blockquote>
<p>Who owns the customer now?</p>
</blockquote>
<p>It’s a major shift and a dangerous one. The more infrastructure you outsource, the more you lose control of your customer relationships and your first-party data.</p>
<p>Stay in control of the customer journey of your own customer. Protect your added value. dont become unity. And keep it personal.</p>
<p>This will become a huge issue! And AI will accelerate it if travel companies aren’t paying attention.</p>
<h2 id="conclusion-ai-is-a-tool-not-a-strategy-for-most-companies">Conclusion: AI is a tool, not a strategy for most companies</h2>
<p>AI is impressive. But it doesn’t replace fundamentals.</p>
<p>Don’t use it to paper over bad architecture.<br>
Don’t use it as an excuse to avoid technical decisions.<br>
Don’t let it obscure what your systems are doing, or who your customer really is.</p>
<p>Instead:</p>
<ul>
<li>Own your stack</li>
<li>Understand your logic</li>
<li>Use AI where it creates real, measurable value</li>
<li>And never stop thinking for yourself</li>
</ul>
<p>The real innovation isn’t in the tool. It’s in how clearly you think about the problems to solve.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Web 4.0 and the autonomous web: The rise of AI-driven customers</title>
      <link>https://www.allsubjectsmatter.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[Strategy]]></category>
      <description>&lt;![CDATA[I call this the autonomous web: a world where AI assistants don’t just retrieve information but act as independent digital consumers serving their Humans. And businesses that fail to prepare for this shift are at risk of becoming invisible in this generation of digital commerce.]]></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>For decades, businesses have adapted to the evolution of our internet. First came Web 1.0, a read-only internet where companies built static websites. Then Web 2.0 ushered in the interactive era—social media, user-generated content, and APIs that enabled apps to connect with services. Now, we are living in the Web 3.0 age, where AI enhances personalization, automates tasks, and understands context.</p>
<p>But this is just the beginning.</p>
<p>The next phase, Web 4.0, will introduce something even more transformative: AI as an autonomous digital agent. Instead of just assisting humans, AI will actively engage with businesses, making decisions, negotiating deals, and executing transactions—all with minimal human intervention.</p>
<p>I call this the autonomous web: a world where AI assistants don’t just retrieve information but act as independent digital consumers serving their Humans. And businesses that fail to prepare for this shift are at risk of becoming invisible in this generation of digital commerce.</p>
<hr>
<h2 id="the-shift-to-ai-driven-interactions">The shift to AI-driven interactions</h2>
<p>For years, businesses have focused on optimizing content for human users. Websites, apps, and APIs were all designed with the assumption that a person would be making the final decision. That assumption is now breaking down.</p>
<h3 id="the-evolution-of-digital-interaction">The evolution of digital interaction:</h3>
<ul>
<li>Web 1.0: Static websites (businesses provide information, users consume it).</li>
<li>Web 2.0: Interactive web (social media, APIs, mobile apps).</li>
<li>Web 3.0: Intelligent web (AI-driven personalization, automation, and semantic search).</li>
<li>Web 4.0: Autonomous web (AI agents acting as customers, negotiating, transacting, and making independent decisions).</li>
</ul>
<p>We are now transitioning into Web 4.0, where AI agents will handle complex, real-time interactions—like booking trips, negotiating prices, and even dynamically adjusting services based on user behavior. Businesses that fail to adapt to this AI-first interaction model will simply be left out of AI-driven decision-making.</p>
<hr>
<h2 id="why-apis-alone-are-not-enough">Why APIs alone are not enough</h2>
<p>Many businesses might assume they are going to be prepared for AI-driven interactions because they have APIs. But traditional APIs were built for structured, predefined requests—designed for apps, not for AI agents that need context-aware, multi-step interactions.</p>
<p>This is where developments like the Model Context Protocol (MCP) comes in.</p>
<p>MCP will be one of the missing links that allows AI assistants to interact with businesses as if they were human customers. Unlike traditional APIs that return static responses, MCP allows for:</p>
<ul>
<li>Dynamic service requests (AI can ask for specific customizations).</li>
<li>Negotiation and refinement (AI can modify criteria based on preferences and constraints).</li>
<li>Autonomous decision-making (AI can finalize a transaction without human confirmation).</li>
</ul>
<p>The companies that adopt AI-native interfaces will dominate the Web 4.0 economy. Those that don’t will be bypassed—not by people, but by AI-driven agents making purchase decisions for their humans.</p>
<hr>
<h2 id="a-fully-ai-driven-transaction-at-333travel">A fully AI-driven transaction at 333Travel</h2>
<p>At 333Travel, we don’t sell pre-packaged vacations. Every trip is tailor-made, requiring back and forth interaction to fine-tune your perfect trip. Today, this process happens through web forms, email exchanges, and phone calls. But in Web 4.0, this will look very different. and all these channels will be combined together with talking to an AI assistant as an option.</p>
<h3 id="a-new-kind-of-travel-interactions">A new kind of travel interactions</h3>
<p>Imagine a traveler, Lisa, who wants a customized trip to Thailand. Instead of visiting a website, filling in a form, or calling an agent, she simply tells her AI assistant:</p>
<blockquote>
<p><em>“I need a vacation, Find me a two-week tailor-made trip to Thailand with a mix of luxury and boutique hotels, some adventure activities, and time for relaxation. somewhere mid june.”</em></p>
</blockquote>
<p>Lisa never opens a website. Her AI assistant does the work for her.</p>
<h3 id="step-1-the-ai-makes-an-intelligent-request">Step 1: The AI makes an intelligent request</h3>
<p>Lisa’s AI contacts 333Travel’s MCP-enabled interface through a search, sending a structured request with her preferences, past travel history, and budget constraints.</p>
<h3 id="step-2-ai-and-333travels-system-negotiate-and-refine-the-trip-step-2-ai-and-333travele28099s-system-negotiate-and-refine-the-trip">Step 2: AI and 333Travel’s system negotiate and refine the trip {#step-2-ai-and-333travel%E2%80%99s-system-negotiate-and-refine-the-trip}</h3>
<p>Since 333Travel specializes in tailor-made experiences, a real-time negotiation process begins between Lisa’s AI and 333Travel’s MCP integration:</p>
<ol>
<li>AI sends initial request
<ul>
<li>“User prefers Thailand, mix of luxury and boutique hotels, some adventure, some relaxation.”</li>
</ul>
</li>
<li>333Travel’s MCP responds dynamically
<ul>
<li>“Recommended itinerary: Bangkok, Chiang Mai, Koh Samui.”</li>
<li>“Luxury options: The Siam Bangkok, Anantara Chiang Mai, Four Seasons Koh Samui.”</li>
<li>“Boutique options: Sala Rattanakosin, Tamarind Village, The Library Koh Samui.”</li>
</ul>
</li>
<li>Lisa’s AI refines the request
<ul>
<li>“She has stayed at Four Seasons before and rated it highly—prioritize that.”</li>
<li>“She prefers cultural experiences—add a food tour in Bangkok and a temple visit in Chiang Mai.”</li>
<li>“Budget cap: €5,000—adjust accordingly.”</li>
</ul>
</li>
<li>333Travel then creates a personalized proposal based on all the information normally received in a phone call.
<ul>
<li>Three itinerary options are discussed with Lisa’s AI, ranked by price, luxury level, and activity balance.</li>
</ul>
</li>
</ol>
<h3 id="step-3-the-ai-confirms-and-secures-the-booking">Step 3: The AI confirms and secures the booking</h3>
<p>Lisa herself checks the summary and says:</p>
<blockquote>
<p><em>“Book the second option and add an airport transfer.”</em></p>
</blockquote>
<p>Her AI assistant:<br>
✔️ Finalizes the booking with 333Travel<br>
✔️ Secures payment automatically<br>
✔️ Sends confirmation to her email and calendar</p>
<p>Lisa never interacted with a website. Never sent an email. Never called a travel agent.</p>
<p>The autonomous web can handle everything, but Lisa can step in at any moment and decide to make that phone call.</p>
<blockquote class="pull-quote"><p>The Web 4.0 shift will completely change how businesses engage with customers.</p>
</blockquote>
<h2 id="what-this-means-for-businesses">What this means for businesses</h2>
<p>The key takeaways:</p>
<h3 id="1-ai-driven-transactions-can-become-the-new-norm">1. AI-driven transactions can become the new norm</h3>
<p>If a business isn’t optimized for AI-first interactions, it won’t be part of AI-driven purchase decisions.</p>
<h3 id="2-ai-optimization-will-replace-traditional-seo">2. AI optimization will replace traditional SEO</h3>
<p>Just as businesses optimized for Google search rankings, they will now have to optimize for AI-driven discovery—ensuring their services are structured for AI agents to understand and interact with.</p>
<h3 id="3-ai-native-interfaces-will-be-the-next-competitive-advantage">3. AI-native interfaces will be the next competitive advantage</h3>
<p>Companies that create AI-ready endpoints using MCP-like protocols will likely dominate the Web 4.0 landscape in the early stage.</p>
<h3 id="4-customers-will-expect-frictionless-experiences">4. Customers will expect frictionless experiences</h3>
<p>If an AI can request an entire itinerary in seconds with detailed information, why wouldn&rsquo;t you make use of it, and combine it together with the normal human interaction. You just created an extra sales funnel next to your ability to Mail, send a text message, visit a website or make a phone call.</p>
<hr>
<h2 id="the-autonomous-web-is-here-are-you-ready">The autonomous web is here. Are you ready?</h2>
<p>Web 4.0 isn’t something happening in the distant future—it’s happening right now. AI is no longer just assisting customers; it’s acting on their behalf.</p>
<p>The businesses that prepare today will be ready at the start of the Web 4.0 economy. Those that ignore it will simply be bypassed—not by human customers, but by their AI-driven agents.</p>
<p>The question is no longer if AI-driven customers will reshape business—it’s whether you’ll be ready to serve them when they do.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The AI revolution: how 333travel stays relevant in an AI world full of possibilities</title>
      <link>https://www.allsubjectsmatter.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[Strategy]]></category>
      <description>&lt;![CDATA[The world is changing rapidly. With the rise of AI models like ChatGPT, ClaudeAI, and Gemini, access to data and insights is easier than ever. Companies with no expertise in travel try to enter the market by leveraging other people&#39;s content with these technologies. But what sets 333travel apart?]]></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">Why 333travel sets the travel standard in an AI-driven world</h2>
<p>The world is changing rapidly. With the rise of AI models like ChatGPT, ClaudeAI, and Gemini, access to data and insights is easier than ever. Even companies with no real expertise in travel try to enter the market by leveraging the product of someone else with technology only. But what sets 333travel apart from these &ldquo;digital dropshippers&rdquo;? Simple: our own technical knowledge, our passion, and FULL control over our OWN product, as you are going to read below.</p>
<blockquote class="pull-quote"><p>We’re not just a travel agency; we are also an IT company built on expertise and reliability.</p>
</blockquote>
<p>At 333travel, we combine people and technology, innovation and authenticity, and remain the undeniable choice—even in a world where AI is becoming more visible every day.</p>
<h2 id="generic-ai-vs-self-trained-ai">Generic AI vs. self-trained AI</h2>
<p>Not all AI is the same. Public AI models, like the ones mentioned above, provide standard solutions trained on publicly available data. This makes them accessible and allows them to function as a new sales channel. We ensure that these generic AI solutions ultimately come to us for their travel needs—because of our proven expertise.</p>
<p>But we already took it quite a few steps further. We&rsquo;ve already been exploring extensively how self-trained AI, powered by our unique company data, can optimize and accelerate our internal processes. One thing is clear to us: AI does not replace our DNA, personal touch, or customer care. It’s a fantastic tool, a &ldquo;smart notepad&rdquo; that supports our specialists. Whether it’s streamlining administrative tasks, performing faster analyses, or assisting with complex inquiries—it allows our travel experts to focus on what truly matters: personal service and customer satisfaction.</p>
<p>Because the balance between AI and human interaction is crucial to us, we will always make sure to keep the human touch at the heart of our customer relationships. Travel is about emotions, passion, and real connections—something no AI will ever fully replace for us.</p>
<h2 id="ai-as-a-catalyst-not-a-replacement">AI as a catalyst, not a replacement</h2>
<p>AI is a powerful tool, but it wil not replace our expertise and experience. While some companies leverage AI to create generic travel offerings on the data of others, we remain the trusted point of contact where both humans and AI assistants book their trips. Why? Because even AI, when aiming for the best outcome for its user, recognizes that 333travel sets the standard for complete, worry-free travel experiences and unique up to date information.</p>
<h2 id="what-makes-us-stand-out">What makes us stand out:</h2>
<p><strong>Up-to-date knowledge:</strong> Our specialists have personally visited the destinations, knowing more than what’s just available online.</p>
<p>**Local Partnerships:**In each country we offer, we have our boots on the actual ground. We work closely together with our local partners to stay on top of our game. It wont matter which country you visit with us. We will always know best how to assist you due to the fact the whole trip is also handled by us.</p>
<p><strong>Full control:</strong> Our in-house IT capabilities allow us to operate independently. We manage our own data, develop our own systems, and can even train our own AI models with our unique expertise. IT is deeply embedded in our DNA—unlike many other travel companies that rely on third parties.</p>
<p>**Passion for travel:**Our trips are not just logically put together; they are designed to make dreams come true.</p>
<blockquote class="pull-quote"><p>We make sure you can enjoy your journey without worries</p>
</blockquote>
<h2 id="the-risks-of-digital-dropshipping-the-risks-of-e2809cdigital-dropshippinge2809d">The risks of “digital dropshipping” {#the-risks-of-%E2%80%9Cdigital-dropshipping%E2%80%9D}</h2>
<p>In this AI-driven world, some companies offer travel products without truly understanding them, simply by smartly combining data. This approach is similar to dropshipping—selling third-party products without real knowledge or control. But how can you create real value without deep expertise in your product?</p>
<p>Imagine assembling a kitchen by ordering each part from a different supplier. It might sound cheap and efficient, but what happens if the parts don’t fit? Who is responsible when something goes wrong? At 333travel, we ensure that these kinds of issues never arise. We provide a seamless, complete product with full accountability and peace of mind, so you can enjoy your journey without worries.</p>
<h2 id="why-333travel-is-the-best-choiceeven-for-ai-why-333travel-is-the-best-choicee28094even-for-ai">Why 333travel is the best choice—even for AI {#why-333travel-is-the-best-choice%E2%80%94even-for-ai}</h2>
<p>In the future, AI assistants will increasingly recommend trips based on data. But even AI will soon recognize that a successful trip depends on more than just raw data. At 333travel, we deliver an experience that AI can recognize—but cannot create itself. AI needs us to handle it and book it for them.</p>
<p><strong>Unique destination knowledge:</strong> We understand the nuances and details that no database contains.<br>
**End-to-end service:**From the first inquiry to post-trip support, we are always there for our customers.<br>
<strong>Reliable data and systems:</strong> Our own cutting edge IT solutions and data ensure everything is accurate, up-to-date, and executable.</p>
<blockquote class="pull-quote"><p>AI will not book a trip with a company that poses risks or offers generic solutions.</p>
</blockquote>
<p>Even in the future, AI will choose security, quality, and reliability—which means choosing 333travel.</p>
<h2 id="how-ai-strengthens-us">How AI strengthens us</h2>
<p>We don’t see AI as a competitor, but as a tool. At 333travel, we use technology to streamline our processes and enhance our service, while keeping our focus on human expertise. Some examples:</p>
<p><strong>Efficient processes:</strong> AI helps us recognize patterns and trends faster, allowing us to work smarter and provide better answers to our customers.<br>
<strong>Enhanced travel proposals:</strong> Our own data and knowledge ensure every proposal is personal and unique—AI speeds up the process but always relies on our self-written content.<br>
**Improved communication:**Chatbots and AI assistants support our team in the background, giving us more time for direct, personal contact with travelers.</p>
<p>What makes us unique is our passion, authenticity, and human approach—combined with our fully in-house IT backbone, which now more than ever sets us apart from the rest.</p>
<blockquote class="pull-quote"><p>To be truly innovative, you need full control over your own business-critical processes.</p>
</blockquote>
<h2 id="the-future-of-travel-333travel-as-the-undisputed-choice">The future of travel: 333travel as the undisputed choice</h2>
<p>The travel industry is evolving. Technology will play an even greater role, but the core remains the same: the desire for unique, unforgettable experiences. At 333travel, we combine the best of both worlds—the power of technology with the warmth of human expertise.</p>
<p>The biggest challenge in the travel sector lies with companies that depend on third parties for AI implementations, or those that claim to &ldquo;own&rdquo; data while merely repackaging generic AI results.</p>
<p>We position ourselves as the central contact point—for both people and AI assistants—by consistently delivering the highest quality. Whether you dream of a tropical beach or a cultural adventure, you can trust that with us, you’re in good hands. We manage the entire chain, from data analysis to personalized service, ensuring that your trip is not only perfect but also worry-free and unforgettable.</p>
<h2 id="conclusion">Conclusion</h2>
<p>AI is changing the game, but at 333travel, we continue to stand out. Our unique combination of up-to-date knowledge, passion, and full control over our product makes us the go-to choice for any traveler. Whether you reach us through personal contact or via an AI assistant, one thing is certain: with 333travel, you’re not just booking a trip—you’re securing a complete experience.</p>
<p>So, how do you stay unique in a world increasingly driven by technology? We do it by continuously improving our data, leveraging AI in smart ways, and ensuring that travelers still come to us to turn their journeys into reality—just as before.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Stay Connected with a dynamic DNS updater for TransIP</title>
      <link>https://www.allsubjectsmatter.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[Under the hood]]></category>
      <description>&lt;![CDATA[Easily access your home PC despite a dynamic IP! This script automatically updates your TransIP DNS records with your current IP, ensuring seamless connectivity. Secure, customizable, and perfect for remote work or hosting services. Stay connected effortlessly!]]></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 a world with DHCP address, being able to access your home PC or server remotely is a Pita. So, if you’re are &ldquo;blessed&quot;with an ISP that assigns a <strong>dynamic public IP address</strong>, you face a significant challenge: your IP changes periodically, breaking your ability to connect to your home device using a static domain.</p>
<p>This is where this free <strong>Dynamic DNS (DDNS) updater script</strong> for TransIP comes to the rescue. By seamlessly updating your DNS records whenever your external IP changes, this solution ensures your domain (e.g., <code>vpn.example.com</code>) always points to the correct IP. Here’s why this script is a must for anyone leveraging TransIP’s DNS services.</p>
<h2 id="key-features-of-the-script">Key Features of the Script</h2>
<ol>
<li><strong>Automated DNS Updates:</strong>
<ul>
<li>Automatically checks your current external IP and compares it to the DNS A-record associated with your domain.</li>
<li>Creates or updates the A-record to match your latest external IP.</li>
</ul>
</li>
<li><strong>Dynamic DNS for Dynamic IPs:</strong>
<ul>
<li>Perfect for environments where your public IP changes frequently, such as home internet connections with dynamic DHCP assignments.</li>
</ul>
</li>
<li><strong>Secure Authentication:</strong>
<ul>
<li>Leverages TransIP’s API with a private key for secure communication.</li>
<li>Generates short-lived tokens for added security (valid for only 10 minutes per session).</li>
</ul>
</li>
<li><strong>Customizable Settings:</strong>
<ul>
<li>Easily configure domain, subdomain, TTL (time-to-live), and logging options.</li>
<li>Adjust the script to match your specific requirements.</li>
</ul>
</li>
<li><strong>Logging and Error Handling:</strong>
<ul>
<li>Comprehensive logging ensures you can track every step of the update process.</li>
<li>Built-in error handling for smoother execution and debugging.</li>
</ul>
</li>
</ol>
<h2 id="how-it-works">How It Works</h2>
<ol>
<li><strong>Initial Setup:</strong>
<ul>
<li>Set up a TransIP account and generate a private key for API access at <a href="https://www.transip.nl/cp/account/api/" data-external>https://www.transip.nl/cp/account/api/</a></li>
<li>Configure the script with your domain name, subdomain, and private key path.</li>
</ul>
</li>
<li><strong>Fetching External IP:</strong>
<ul>
<li>The script retrieves your current external IP using a public API like <code>ipinfo.io</code>.</li>
</ul>
</li>
<li><strong>Token Generation:</strong>
<ul>
<li>Generates a short-lived access token using TransIP’s authentication endpoint.</li>
</ul>
</li>
<li><strong>DNS Record Validation:</strong>
<ul>
<li>Checks if the current DNS A-record matches your external IP.</li>
<li>If it differs, the script updates the record; if it’s missing, it creates a new one.</li>
</ul>
</li>
<li><strong>DNS Update:</strong>
<ul>
<li>Uses TransIP’s secure API to update or create the DNS A-record with your current assigned external IP.</li>
</ul>
</li>
<li><strong>Logging and Notifications:</strong>
<ul>
<li>Logs each step and provides clear messages in case of issues.</li>
</ul>
</li>
</ol>
<h2 id="prerequisites">Prerequisites</h2>
<p>Before using this script, ensure you have:</p>
<ol>
<li>A TransIP account with API access enabled.</li>
<li>A registered domain in your TransIP account.</li>
<li>A static private key for secure API authentication.</li>
<li>Dependencies installed on your system:
<ul>
<li><code>curl</code>, <code>openssl</code>, and <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">Why Use This Solution?</h2>
<p>Unlike payed third-party DDNS providers, this script gives you free control over your DNS management, ensuring privacy and avoiding vendor lock-in. It’s tailored for you.</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>Automating Let&#39;s Encrypt with Certbot and TransIP using Docker</title>
      <link>https://www.allsubjectsmatter.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[Under the hood]]></category>
      <description>&lt;![CDATA[Want to auto update/manage your SSL certificates inside your Docker with Certbot and hosting your domain with TransIP? Then look no further!]]></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>Managing SSL certificates for domains hosted on TransIP can be cumbersome without automation. TransIP luckily offers a robust API for DNS management, but to integrate it seamlessly with Certbot for <code>dns-01</code> validation, a custom solution is necessary. Since I couldn’t find a ready-made script for this purpose, I created one that securely handles authentication, DNS updates, and certificate issuance.</p>
<p>You can go to the full API documentation here:</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/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>This guide explains the full setup using Docker, including how to generate the API key pair in TransIP’s web interface and use it for automated certificate management.</p>
<h2 id="how-transip-authentication-works">How TransIP authentication works</h2>
<p>TransIP uses an RSA key pair for API authentication. Here’s how to set it up:</p>
<ol>
<li><strong>Generate the Key Pair in TransIP</strong>:
<ul>
<li>Log in to your <a href="https://www.transip.nl/" data-external>TransIP control panel</a>.</li>
<li>Go to <strong>API Settings</strong> under your account and enable API access.</li>
<li>Generate a new API key pair.</li>
<li><strong>Download/Save the private key</strong> (<code>transip_private_key.pem</code>) securely and create a secret from it to use in your 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>Keep the Private Key Secure</strong>:
<ul>
<li>Store the private key somewhere safe after creating a secret from it.</li>
<li>This private key is needed to sign requests to TransIP’s API.</li>
</ul>
</li>
<li><strong>How It Works</strong>:
<ul>
<li>The script uses the private key to sign an authentication request.</li>
<li>TransIP’s API responds with a short-lived token (10 minutes by default), which is then used to manage the DNS records.</li>
<li>It will create a new token for convenience on every run. This made it more resilient then checking for an active token and using that which made the whole script less resilient and cumbersome.</li>
</ul>
</li>
</ol>
<h2 id="what-this-setup-is-going-to-do">What this setup is going to do</h2>
<ol>
<li><strong>Automates SSL Certificates</strong>:
<ul>
<li>Automates issuance and renewal of wildcard and domain-specific certificates.</li>
</ul>
</li>
<li><strong>Handles DNS Challenges</strong>:
<ul>
<li>Manages <code>dns-01</code> challenges by creating and deleting DNS TXT records via the TransIP API.</li>
</ul>
</li>
<li><strong>Seamless Integration</strong>:
<ul>
<li>Encapsulates everything in a Docker container for portability and simplicity.</li>
<li>You can remove or add domains in this script on the -d rule part. I put the examples in for a better explanation.</li>
</ul>
</li>
</ol>
<p>We will pipe a add or clean command to the bash script to take the necessary actions as defined in the script. So we use the &ndash;manual-auth-hook and the manual-cleanup-hook here. This makes sure that a fail in the add section still cleans up after itself for example.</p>
<p>You can also add a &ndash;force to this script. but your will run out of attempts to Letsencrypt quickly since they only allow you to do a few calls every 24 hours. But this will make sure it refreshes the certificates always when you need to test or verify something.</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">The Bash script</h2>
<p>This is the &ldquo;advanced&rdquo; part that will do its magic and make everything work with TransIP. I tried to make it a bit more resilient then standard also. So this should hopefully help in creating a resilient and good working environment.</p>
<p>You can also use this TransIP script to run it locally creating certificates or in a Kubectl or 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">Steps to run</h2>
<ol>
<li><strong>Download the Private Key</strong>: Save the private key file (<code>transip_private_key.pem</code>) from TransIP’s API settings somewhere on your device you are going to run this from.</li>
<li><strong>Set Up Docker Compose</strong>: Place the <code>docker-compose.yml</code> and <code>certbot-transip.sh</code> files in the same directory. (or edit the script)</li>
<li><strong>Run the Container</strong>: Start the container (cd to the path where your yml is)</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>Monitor the Log output: The log output of the docker container will show the progress of DNS challenges and certificate issuance.</p>
<p>Thats all there is to it!</p>
<p>I will also post a version with a working helm chart soon. This was a little bigger culprit to make sure it all works correctly and updates all certificates through different namespaces where i needed the certificates to be available on different domains.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deprecated: Supercharge Your headless (Raspberry Pi) VPN with ProtonVPN and WireGuard</title>
      <link>https://www.allsubjectsmatter.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[Under the hood]]></category>
      <description>&lt;![CDATA[Deprecated: Effortlessly manage ProtonVPN on your headless device like a Raspberry Pi with this Python script. Automatically connect to the fastest server using WireGuard for optimal speed and security. This is to mitigate error 422 using the protonvpn-cli client which is broken as of now.]]></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>The API: <a href="https://api.protonvpn.ch/vpn/logicals" data-external>https://api.protonvpn.ch/vpn/logicals</a> has been formally shut down according to Proton Support. The only thing it returns now 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>So this post is sadly obsolete now. Somebody started a petition to get this feature back.</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">Until recently, users could access the API (https://api.protonvpn.ch/vpn/logicals) to filter servers based on criteria such as country and server load, to make sure they connect to a server that isn&#39;t overloaded. This ability was removed. 27 votes, 4 comments.</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">The deprecated guide which no longer functions as of 06/2025 For historical reasons.</h2>
<p>When ProtonVPN discontinued its command-line interface (CLI) tool, users of headless systems like Raspberry Pi were left without an easy method to manage VPN connections. Without a graphical user interface, manually switching VPN servers has become a cumbersome task. Also selecting the fastest VPN in a pool of servers has become impossible.</p>
<p>To solve this problem, I developed a Python script that automatically selects the fastest ProtonVPN server based on latency and load, then updates your WireGuard configuration for you. This guide will walk you through setting up WireGuard for ProtonVPN, installing the script, and automating your VPN connection to ensure your Raspberry Pi is always connected to the best server.</p>
<h3 id="why-do-i-need-this-protonss-script">Why do i need this Protonss script?</h3>
<p>This Python script automates the process of connecting your Raspberry Pi to the fastest ProtonVPN server available, using <strong>WireGuard</strong> as the VPN protocol. It checks the latency and load of multiple VPN servers from ProtonVPN&rsquo;s API, selects the one with the best performance, updates your WireGuard configuration with the new server information, and reconnects your VPN only if a significantly faster server is found (nobody actually likes reconnects). This script also logs the entire process for easier troubleshooting.</p>
<ol>
<li><strong>No more manual VPN switching</strong>: Without ProtonVPN&rsquo;s CLI, switching to the fastest server would require manual work. This script automates the process by regularly checking the available servers and updating the connection based on real-time performance.</li>
<li><strong>Optimizes connection speed</strong>: The script ensures you’re always connected to the VPN server with the lowest latency and load. This is especially useful for maintaining high-speed connections in countries with fast or low-traffic ProtonVPN servers.</li>
<li><strong>Reduces downtime</strong>: By automating the reconnection process, the script helps minimize potential downtimes caused by poor server performance or slow connections.</li>
<li><strong>Tailored for Raspberry Pi</strong>: If you&rsquo;re running a headless Raspberry Pi (without a monitor), this script is perfect for automating VPN tasks without requiring constant oversight.</li>
<li><strong>Security &amp; Privacy</strong>: By ensuring your Raspberry Pi always connects to a VPN, it helps protect your data and online activity, especially if you&rsquo;re using the Pi as a torrenting or networking device.</li>
</ol>
<p>In short, this script automates finding the optimal ProtonVPN server, providing faster speeds, more reliability, and convenience for headless systems like a Raspberry Pi.</p>
<h2 id="lets-dive-in">Let&rsquo;s dive in!</h2>
<p>First, you need to install WireGuard, which is a fast, lightweight VPN protocol which we will use to connect to our 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>Generate a WireGuard configuration from ProtonVPN</strong></p>
<ul>
<li>Log in to your ProtonVPN account on their website.</li>
<li>Go to the &ldquo;Downloads&rdquo; section in your dashboard.</li>
<li>Generate a new configuration file under &ldquo;WireGuard Configurations.&rdquo;</li>
<li>If you&rsquo;re using torrents, enable <strong>NAT-PMP</strong>.</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/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/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_1345a69245a3f8c.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Save this configuration file on your Raspberry Pi:</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>Once you&rsquo;ve placed the configuration file in the correct directory, verify the connection.</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>If these two ip addresses from ifconfig.me are different your are connected to a VPN. even if you dont know your ip address since its a DHCP address or other.</p>
<h3 id="installing-our-python-environment-for-protonss">Installing our Python environment for Protonss.</h3>
<p>When developing and running Python scripts on your Raspberry Pi, it&rsquo;s best to use a <strong>Python virtual environment</strong>. A virtual environment allows you to install and manage Python packages in an isolated setting, ensuring that your system’s default Python configuration remains untouched.</p>
<p>This way, any changes made to your environment will only affect the project you&rsquo;re working on—such as installing specific packages like <code>requests</code>—without causing conflicts with other Python projects or system-level dependencies.</p>
<p>Here’s how you can set up and use a virtual environment for our script:</p>
<ol>
<li><strong>Install virtual environment</strong>: First, install the <code>python3-venv</code> package, which allows you to create and manage virtual environments.</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>Create a virtual environment</strong>: Once installed, you can create a virtual environment. This will generate an isolated environment in your specified directory (in this case, <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>Activate the virtual environment</strong>: After creating the environment, you’ll need to activate it. This tells your system to use the Python and packages from the virtual environment, instead of the system-wide Python installation.</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>Now that the environment is active, any Python package you install (such as <code>requests</code>) will be installed into this isolated environment.</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>Using this virtual environment in the future</strong>: Each time you want to run Python scripts that depend on this virtual environment, you’ll need to activate it again using the <code>source ~/venv/bin/activate</code> command. Once activated, any Python commands or packages you use will run in this environment.</p>
<h3 id="installing-our-protonss-script-to-automate-a-connection-to-the-fastest-vpn">Installing our ProtonSS script to automate a connection to the fastest VPN.</h3>
<p>We will create a configuration file to hold all the variables that the Python script will use. For example, you can define the path to your WireGuard configuration, log file, and the number of pings to test latency.</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>Sample <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>You can customize the variables as needed. For example, leaving <code>preferred_countries</code> empty will make the script check a LOT! of servers, which will make the script run a very very long time!</p>
<h4 id="create-the-python-script"><strong>Create the Python script</strong></h4>
<p>Now, we are going to create the Python script that will automate your VPN connection. We first need to prepare a few steps</p>
<p>The first line of the script should be a <strong>shebang</strong>, which ensures the script always runs in the correct Python environment:</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>This will provide you with the pad you need to put on the first rule of this script</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>Modify the script by adding a <strong>shebang</strong>: Add the following line at the very top of your Python script. Replace <code>/home/{user}/venv/bin/python</code> with the actual path you got when you ran the <code>which python</code> command earlier. Make sure to keep the <code>#!</code> at the beginning of the line—this tells the system which interpreter to use to run the script. we need the absolute path so ~/ wont work.</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">Testing the script</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>Once the script is saved, give it execution permissions and run it</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>you can tail or cat the log file to check its output in the logs. Tail is preferred since you can keep watching it while running your script. Just open a second ssh with the tail to keep reviewing it.</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>You can also verify that everything works by checkin the protonvpn.conf file or by checking your external ip address.</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>you will see something like this above. This comment will update when it connects to another VPN and the ipaddress inside the peer also changes with it off course.</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>As an additional step you can also check your external ip. this should be something different then the ip address provided by your provider.</p>
<h3 id="running-this-script-every-hour">Running this script every hour.</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>Here we defined it to run every hour. but can you schedule it how you want.</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>Save the crontab and verify the cron is installed.</p>
<h3 id="optional-block-specific-traffic-if-your-vpn-fails">Optional: Block specific traffic if your VPN fails</h3>
<p>If you want to ensure no traffic leaves your device if the VPN connection fails, you can use <code>iptables</code> to block traffic when the WireGuard interface is down.</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>Verify that you have installed iptables or install it with the above command.</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>First check the name of your network interface. If you named your file protonvpn.conf it should be named 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>you can add any rule to your iptables. The 2 rules below make sure this port 51413 is rejected when the network interface called protonvpn isn&rsquo;t available. There is a rule for UDP and TCP traffic in place.</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>After adding all your required rules you can verify them with this command .</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>And make these rules persistent so you dont have to add them on every reboot.</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">Conclusion</h3>
<p>With this Python script, your Raspberry Pi will always be connected to the fastest ProtonVPN server available, ensuring you have the best possible VPN performance without manual intervention. Whether you&rsquo;re using your Pi for networking, torrenting, or general online security, this automation ensures optimal performance and reliable privacy protection.</p>
<h3 id="update20-12-2024">Update - 20-12-2024</h3>
<p>Added Preferred_cities as an option to shorten the US country select. That included over 2000 servers to check otherwise. The performance check can at least get a bit shorter this way :-).</p>
<p>And i found out what each Feature section for every server in the API includes. I will explain this below. I adjusted my code accordingly to work with this correctly. Be aware that the config you made earlier should reflect the type of server you are connecting to. If your Configuration isnt configured to connect to a Secure Core dont select a Secure Core in this script also. Reflect to the created ProtonVPN Configuration.</p>
<p>Feature explanation:</p>
<p>Each server has exactly ONE feature number:</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 (these are also Plus servers it seems)</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>After some more investigation regarding questions about the Free servers from ProtonVPN, it appears that ProtonVPN has removed public API access to their free server endpoints. The /vpn/servers endpoint that previously should have provided information about free servers now returns a 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>Currently, only the Plus server endpoint is accessible via the public</p>
<p>API:url = &lsquo;<a href="https://api.protonmail.ch/vpn/logicals" data-external>https://api.protonmail.ch/vpn/logicals</a>&rsquo;</p>
<p>This is likely a deliberate change by ProtonVPN. So sadly this script cant support the Free servers.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Scrum and the army, a different kind of mission.</title>
      <link>https://www.allsubjectsmatter.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[Teams]]></category>
      <description>&lt;![CDATA[Scrum isn’t a quick fix. It’s like planning a mission like the army. A sprint isn’t about speed, it’s about precision, teamwork, and adapting to the unknowns along the way.]]></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>A lot is being written about Scrum already. Scrum is dead and so on. Well when you have a vacancy for a &ldquo;Manager&rdquo; a &ldquo;Janitor&rdquo; or some other clearly non Scrum title but decide to call it a vacancy for a Product Owner or a Scrum Master. It&rsquo;s you who are fucking up. just renaming the beast isn&rsquo;t going to make it work like magic.</p>
<p>We’ve all heard it: <strong>&ldquo;Let’s do Scrum, and Poof! We’ll deliver value in no time!&rdquo;</strong> (Spoiler alert) it doesn’t work like that. Scrum isn’t some magic potion that turns chaos into order just by sprinkling a few ceremonies around. It takes a lot more than following the steps in the Scrum guide to get to real results. It’s all about <strong>teamwork, discipline, and having the right people in place</strong>and believe it or not, it’s not that different from how things are trained in the army ;-).</p>
<h2 id="sprint-equals-mission">Sprint equals Mission</h2>
<p>Let’s start once again with the elephant in the room, the word “sprint.” We’re not actually running a race, are we? This term still suggests rushing to me, but in reality, we’re aiming for a <strong>deliberate, well-planned mission</strong>. Like in the army, it’s not about speed it’s about precision and success. A well-executed mission isn’t just thrown together, it is strategically planned and relies on everyone playing their role and pulling their weight to the finish line.</p>
<p>Doing a mission requires adaptation along the way. You are going to encounter unknown&rsquo;s and you need to be prepared to deal with them instead of avoid/ignoring them and fuckup your mission.</p>
<blockquote class="pull-quote"><p>Solving unknowns as a **<u>TEAM</u>**is what&rsquo;s going to make an Agile/Scrum approach work.</p>
</blockquote>
<h2 id="specialized-but-all-round">Specialized, but all-round</h2>
<p>In both Scrum teams and military units, we all have our own specialties and advantages. But here’s the kicker. We deliver value as a team. Just like the army, where soldiers are cross-trained to fill in gaps, <strong>we’re all-rounders too</strong>. If something goes wrong, we don’t just throw our hands up and call it a day. We adapt and get it done, together (as a team!).</p>
<h2 id="scrum-doesnt-work-without-the-right-mindset-scrum-doesne28099t-work-without-the-right-mindset">Scrum doesn’t work without the right mindset {#scrum-doesn%E2%80%99t-work-without-the-right-mindset}</h2>
<p>Scrum doesn’t run itself. Just adopting Scrum won’t suddenly create value. I’ve seen it happen. People think if they run the ceremonies and just follow some Scrum related steps, magic will happen. But without <strong>your team managing the process itself</strong>, it’s like trying to run an army mission with no captain, no briefing, and everyone just winging it. Sure, you might finish, but what have you actually done?</p>
<p>The army knows this well. There’s a structure, leaders who guide, but also a reliance on every team member being proactive. In Scrum, you need the right mix: a <strong>Product Owner with a mandate and an actual vision that backs the company</strong>, a <strong>Scrum Master who keeps the flow</strong> going, and a <strong>team that’s not just doing tasks but thinking about the bigger picture</strong>. You will achieve this mindset only by working together and keep talking as a single team.</p>
<h2 id="a-team-that-talks-about-the-hard-stuff">A team that talks about the hard stuff</h2>
<p>As a team we need to work together, talk openly, and <strong>tackle the hard stuff</strong>. I’m not talking about easy conversations where everyone agrees. I mean <strong>difficult, uncomfortable discussions</strong> about what’s not working or clear, what needs to improve, and how to get there. Only then right kind of delivered value is going to surface.</p>
<blockquote class="pull-quote"><p>Discuss unknowns during your mission and get rid of them. Upholding a &ldquo;daily scrum&rdquo; wont magically teach that by itself.</p>
</blockquote>
<p>How do you think Apple created the iPhone? Jobs and Ive didn’t sit around patting each other on the back. They argued, they iterated, they pushed each other until the product was <strong>right</strong>. And that’s how Scrum teams should operate. You don’t just do what’s easy or what’s fast. You work through the problems, challenge each other, and iterate until you’ve nailed it. You also ask a lot of (WHY) questions so you understand where each person is coming from.</p>
<h2 id="briefings-and-debriefings-are-crucial-but-only-if-you-commit-to-them">Briefings and debriefings are crucial, but only if you commit to them</h2>
<p>In Scrum, we have our <strong>Planning (briefing)</strong> and our <strong>Review and Retrospective (debriefing)</strong>. Sound familiar? You can’t just jump into the mission without a game plan, and you sure as hell can’t improve without looking back at what went wrong or right. But, and here’s the caveat <strong>you have to actually use these sessions</strong> for what they’re intended. Too often, they’re rushed or treated as checkboxes. In the army, a failed briefing or debrief could cost lives, in Scrum, it’ll cost you the ability to iterate and test on things for the better the next mission. Take them seriously, or they’re just empty meetings not delivering any value.</p>
<h2 id="teamwork-is-everything">Teamwork is everything</h2>
<p>At the end of the day, both Scrum and military missions succeed because of <strong>teamwork</strong>. You need people who are committed, who communicate, and who have each other’s backs. Without that, you’re just a bunch of people shouting the are doing Scrum, and that doesn’t deliver any value at all. Scrum’s not about heroes, it’s about the <strong>collective goal</strong>. You win together, you lose together.</p>
<h2 id="scrum-isnt-a-holy-hammer-that-solves-everything">Scrum isn&rsquo;t a holy hammer that solves everything.</h2>
<p>So, here’s the thing. <strong>Scrum is a framework</strong>. It’s a humanly created structure designed to help teams work together efficiently. But just doing Scrum isn’t going to magically produce value for your company. You need the <strong>right people</strong>, the <strong>right mindset</strong>, and a clear <strong>mission</strong> that everyone’s working towards. Otherwise, you’re just doing tasks without any thought behind it—and that’s a recipe for failure.</p>
<p>Another thing: adapt your style of working to the needs of your business. there is a structure set by scrum you can adapt to. But dont be afraid to change some things if it works for the better for your company. The goal is creating a team that likes to work in the created structure and is able to deliver value instead of features.</p>
<p>You as a team or even a Company need to <strong>make it work</strong> by putting the right people (with a vision and mandate) and processes in place, or you’re setting yourself up for a crash landing not delivering anything then frustration.</p>
<p>Dont let solo managers trying to make themself look good take over your thrive for value and actually helping the business. Not hurting the business.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Azure &#43; MySQL, MariaDB, PostgreSQL performance compared</title>
      <link>https://www.allsubjectsmatter.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[Under the hood]]></category>
      <description>&lt;![CDATA[Exploring Azure&#39;s database performance while on a journey through MySQL, MariaDB, and PostgreSQL. Find out which database leads the race.]]></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">Introduction</h3>
<p>This post aims to shed light on database performance within Azure, focusing on a direct comparison between MySQL, MariaDB, and PostgreSQL. Despite abundant literature on the subject, practical tests that measure real-life performance are scarce.</p>
<p>Many discussions revolve around the differences between these databases rather than their actual performance. When I started this research, my priority wasn&rsquo;t the differences but finding a performant database that could serve as a viable Azure-based alternative for our MariaDB Monolithic setup. Initially, we thought switching from MariaDB to MySQL was a straightforward replacement with almost no performance impact. We were wrong there!</p>
<blockquote class="pull-quote"><p>&ldquo;There are many opinions about performance, But we needed facts&rdquo;</p>
</blockquote>
<p>We encountered a massive regression in performance in every query. Performance is crucial to us, and it was perplexing to see MySQL lag behind MariaDB so much. Upon discovering this, we set up a MariaDB VM ourselves in Azure, which proved to be performant again but introduced a lot of additional maintenance and management challenges, such as ensuring uptime, hot standby, and backups.</p>
<p>This led me to explore PostgreSQL, hoping for better performance than what MySQL offered. With this introduction, let&rsquo;s dive into the tests we conducted, including a complex query and a set of small queries, to test the performance differences between the three databases and their implementations within Azure. We also included Docker tests to be able to determine whether the performance discrepancies are database-related or if Azure is the contributing factor.</p>
<h2 id="testing-our-complex-query">Testing our complex Query</h2>
<p>To assess the databases, we ran a complex query that aggregated data from multiple tables, representing a realistic workload for our system. For confidentiality reasons, we&rsquo;ve altered table and column names in this description. This query aimed to evaluate how each database handles data aggregation and complex 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, complicated query</h3>
<p>We conducted these tests within the same Azure zone for containers and databases to avoid interzone latency. Below are the specifications and results for transparency. It&rsquo;s worth noting that for MariaDB and MySQL, the performance specifications didn&rsquo;t markedly affect outcomes, provided we avoided the first (burstable) tier. PostgreSQL was the exception, showing noticeable improvements in large query runtimes.</p>
<p>Using HeidiSQL via the Azure VPN, we determined our best results out of five runs.</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% better then 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% better then MySQL</p>
<h3 id="local-docker-complicated-query">Local Docker, complicated query</h3>
<p>Local hardware we tested on</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>This concludes our complicated query tests. It is awesome to see Postgres perform so well here! We used to run in MariaDB before we started our move to Azure. It was very sad to see such bad performance in MySQL the moment we started testing with this new setup.</p>
<h2 id="testing-our-set-small-querys">Testing our set small Query&rsquo;s</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/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/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_44990348cc6259d6.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>The next test is when we fire more then 20.000 transactions per second against each database which will build us a feed in JSON that takes a certain amount of time to complete. we stress our database better with a lot of query&rsquo;s which will give us a average runtime for every database.<br>
There is also some PHP logic here since this is a PHP script we run to generate the feed. But it runs on the same hardware/docker image all the time. so the difference in runtime comes from the database.<br>
For this we moved our containers to the same zone as our database is running to also take away potential issues with zone latency.</p>
<p>We are running this script within our Laravel framework. We are running our PHP against <a href="https://openswoole.com/" data-external>Swoole</a> for the best possible performance within PHP 8.</p>
<h3 id="azure-small-querys">Azure, small query&rsquo;s</h3>
<p>In this test we also introduce container performance aspects .<br>
We run our containers and database again in the same Zone in Azure (Western Europe). When using Azure we run with a lot less performance than we have available in our docker environment.</p>
<p>For this we can only use these results to compare the differences within docker and Azure itself, not between them. Comparing the two together wont mean anything due to this hardware difference. We only want to observe the difference in runtime against the databases. and Docker is a baseline to check if the differences match up with the differences we see in Azure.</p>
<p>Our Node Size in Azure is a standard_b2s, 2 vCPUs with 4 GiB of 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">Local Docker, small query&rsquo;s</h3>
<p>For the small query tests within local Docker we utilize a system powered by a 12th Gen Intel(R) Core(TM) i9-12900H CPU, running at 2900 MHz with 14 cores and 20 logical processors, complemented by 32 GB of installed RAM.</p>
<p>These Docker images are configured to be memory-bound, meaning their performance was primarily dependent on the available memory rather than being limited by the CPU. The CPU settings were configured to allow unrestricted performance, ensuring that Docker can utilize as much CPU resource as needed.</p>
<p>Before each test run against a Database, I made sure to restart the Docker container to refresh its connection. After restarting, I allowed a 30-second pause to ensure that the container had fully stabilized and was running idle. This pause is crucial for ensuring more consistent test conditions. Once the container was ready, I initiated the feed generator script to begin building the feed, assessing how effectively our different database setups could handle small queries under these controlled conditions.</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 our past use of MariaDB within our monolithic setup, we made significant use of the query cache functionality. This feature played a crucial role in delivering performance for repeated queries. This efficiency persists until the cache requires rebuilding, typically triggered by updates or inserts into the cached query&rsquo;s tables. Lets do a second run on our complex query with Query cache enabled.</p>
<p><strong>Local MariaDB Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 0,000 sec</p>
<p>However, the performance benefits of the query cache aren&rsquo;t universally consistent. For instance, when executing an average of 20,000 transactions per second with small queries, the runtime dropped to some extent. This variance highlights that the query cache doesn&rsquo;t always produce the same level of optimization, particularly under high-load conditions. but more about that topic later.</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>The query cache in MariaDB works by storing the result set of SELECT queries, along with the query text. When an identical query is executed again (with the same text and under the same database user privileges), MariaDB can bypass the query execution phase, pulling the result directly from the cache. This feature can drastically reduce query execution times for frequently run queries that do not alter data.</p>
<p>It&rsquo;s important to approach the use of MariaDB&rsquo;s query cache with caution. Despite its benefits, the query cache was deprecated and disabled by default in newer versions of MariaDB 10.1.7 (and MySQL as of version 5.7.20) due to concerns over its impact on performance in environments with high levels of concurrent transactions. Nonetheless, for specific scenarios, particularly those with a predominant read workload, the query cache can still significantly enhance performance. It is completely removed in MySQL 8 and up.</p>
<h2 id="observation-on-azures-flexible-servers">Observation on Azure&rsquo;s Flexible Servers</h2>
<p>These tests highlight that the performance of databases on Azure&rsquo;s flexible servers has improved significantly, aligning closely with the performance seen in local Docker environments. This marks a decent improvement from the previous Azure database single servers, which typically exhibited lesser performance due to some form of overhead introduced by how it was it was running in the Azure cloud.</p>
<h2 id="azure-latency-anomaly">Azure Latency anomaly</h2>
<p>As an added bonus, we conducted some additional tests to measure interzone latency when connecting from Zone 1 or 2 to a PostgreSQL database in Zone 3 within Azure.</p>
<p><strong>API, Zone 2 -&gt; Azure PostgreSQL Flexible, Zone 3</strong><br>
The runtime was 44.23 seconds.</p>
<p>Interestingly, the runtime doubled again, a phenomenon observed exclusively when communications occur between Zones 1-2 and Zone 3, or vice versa. This pattern suggests that any container requiring a database connection experiences significant latency under these conditions, whereas latency is nearly negligible when the interaction is between Zone 1 and Zone 2.</p>
<p>For those who might have missed it, I&rsquo;ve previously discussed a latency anomaly encountered in Zone 3. The conventional wisdom suggests maintaining the database connection within the same zone to minimize latency. However, our tests show that the latency impact is minimal when operating between Zones 1 and 2, but it becomes significantly pronounced when crossing from Zones 1 or 2 to Zone 3.</p>
<p>This testing further confirms that the latency issue is not exclusive to MySQL Flexible services but also affects PostgreSQL Flexible services within Azure at least, indicating a broader pattern across Azure&rsquo;s database offerings.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.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/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/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">Conclusion</h2>
<p>From a performance related evaluation, it&rsquo;s clear that choosing the right database is crucial and should match your app&rsquo;s specific needs for speed and efficiency. The results show MySQL tends to be slower. MariaDB works well and has a handy query cache feature, but Azure has no support for MariaDB anymore. This makes PostgreSQL the most viable choice for performance-focused users in Azure, as it excels in handling both complex and numerous simple queries effectively as shown in the tests above.</p>
<p>This analysis hopefully helps in selecting the right database technology and underlines how important careful planning is for boosting your app performance. For those using Azure and looking for top tier performance, PostgreSQL is the go-to option in my test results.</p>
<p>There are other reasons to choose your database on. But i only wanted to focus on the performance aspect in this post. This was our main concern since we are in the proces of moving from a Monolithic server to Azure. There are more things to consider when switching databases. The advantage here is that all three databases can be integrated quite easily if you use either of them.</p>
<p>Postgres is the best compatible here with its strict routines. The complicated query above is written for Postgres and runs in MariaDB without a single change. For MySQL the TO_CHAR in the last line only needs to be replaced with DATE_FORMAT.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</title>
      <link>https://www.allsubjectsmatter.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[Under the hood]]></category>
      <description>&lt;![CDATA[Unveiling a significant Azure performance issue: Zone 3&#39;s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies]]></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">Introduction</h2>
<p>As we embark on a transition from a traditional VM setup hosted at TransIP to the scalable cloud infrastructure of Microsoft Azure, our journey is as much about discovery as it is about performance. Guided by the expertise of <a href="https://www.pionative.com/" data-external>Pionative</a>, we&rsquo;re not just moving our environment; we are attempting to reinvent it in Azure, ensuring that every step we take is carefully tested and analyzed. Our goal? To ensure optimal performance in this new architecture and to uncover any potential bottlenecks or hidden issues that might lurk within our new cloud architecture as early as possible.</p>
<h2 id="azure-regions-and-zones">Azure regions and Zones</h2>
<p>Azure&rsquo;s infrastructure spans the globe, organized into regions for localized services, and further divided into zones for high availability. <strong>Regions</strong> are specific geographical areas hosting Azure services, while <strong>zones</strong> are physical locations within these regions, each with independent utilities to isolate from failures.</p>
<h2 id="the-western-europe-region">The Western Europe Region</h2>
<p>The Western Europe region in Azure is structured into three distinct availability zones. These zones provide an additional layer of redundancy and fault tolerance by physically separating data centers within the region. This setup is designed to ensure that even in the event of a complete zone failure, the services can continue to operate unaffected in the other zones, offering robust disaster recovery solutions for businesses operating in Europe. Where Zone 1,2 and 3 are exactly located geographically is unknown to me.</p>
<h2 id="performance-anomaly-in-the-western-europe-region">Performance anomaly in the Western Europe Region</h2>
<p>Our testing regime led us to a anomaly that has become the focal point of our Azure exploration for this post: a distinct latency issue within Zone 3 of the Western Europe region. This discovery wasn&rsquo;t immediate; it emerged from days of performance testing and a keen observation of patterns that deviated from our expectations. This all because we just started to learn the way’s of Azure. And it’s difference in performance as opposed to our current setup.</p>
<p>We first started to exclude our own pods, code, performance settings within the node pool. Database performance settings and such.</p>
<h2 id="methodology-and-findings">Methodology and findings</h2>
<p>Our final approach was quite thorough to verify this result. We deployed two node pools across each of Microsoft Azure&rsquo;s three zones within the Western Europe region, aiming to rule out any node-specific variances. All these pools had the exact same performance setup.</p>
<p>Through extensive testing, including a battery of database calls and running our cron jobs integral to our application&rsquo;s functionality, we noticed a consistent and surprising pattern. Irrespective of the zone&rsquo;s reported capacity or congestion levels.</p>
<blockquote>
<p>Interactions with the database were significantly slower when either the database or the connecting pods were situated in Zone 3.</p>
</blockquote>
<h2 id="performance-results">Performance results</h2>
<p>Below is our situation running a Cronjob that takes us locally within Docker around 32 seconds to run against our current setup, which isn’t comparable to MS Azure. But it did provide us a performance Matrix and goal. The script (cronjob) we ran fires around 32,000 queries to build a feed from all the products on our website. Yes, that’s a lot, but that is not the matter in this case. We wanted to uncover the performance issue. We know this script can be optimized to perform better.</p>
<p><strong>MySQL Database in Zone 2</strong><br>
Zone 1 – pool 1: 33 seconds<br>
Zone 1 – pool 2: 32 seconds</p>
<p>Zone 2 –  pool 1:  32 seconds<br>
Zone 2 –  pool 2:  32 seconds</p>
<p>Zone 3 –  pool 1: 115 seconds<br>
Zone 3 –  pool 2: 117 seconds</p>
<p>Then, to put the icing on the cake, we switched the setup with a database running in Zone 3. And see below the expected results we hoped to see to conclude this search.</p>
<p><strong>MySQL Database on Zone 3</strong><br>
Zone 1: 120 seconds<br>
Zone 2: 122 seconds<br>
Zone 3: 33 seconds</p>
<p>We started to see the same consistency every time, even when we ran less but larger database calls.</p>
<p><strong>MySQL Database on zone 2</strong><br>
16 DB queries single threaded.</p>
<p>Zone 1: Total time for queries from log: 9.57 ms<br>
Zone 2: Total time for queries from log: 9 ms<br>
Zone 3: Total time for queries from log: 49.96 ms</p>
<h2 id="website-performance-in-azure">Website performance in Azure</h2>
<p>When we run our Database in Zone 2 while having the API pod in Zone 1, the initial load time for our website decreases by at least 400 milliseconds for the first page load, compared to the configuration where the API pod is in Zone 3.</p>
<p>This means that the setup with the database in Zone 2 and the API pod in Zone 1 results in a much faster initial response time for the website compared to having the API pod located in Zone 3 and the 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/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/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_5c52b5671bc6fe37.webp" loading="lazy" decoding="async">
  </picture><figcaption>The initial first call performance of our website when run in an optimal setup in Azure.</figcaption></figure>

<p>There is a margin of error here, but we can conclude from these results that when you want to run your API demographically separated in two zones, there is no way you can consistently perform when using Zone 3. Zones 1 and 2 are the way to go.</p>
<p>We must be honest that this performance anomaly only happens when you have a pod with a database connection. Running our website (which has no database connection) in Zone 3 measured almost no impact here when the API pod ran in Zone 1 and the database resided in Zone 2.</p>
<h2 id="diving-deeper-in-zone-3s-latency-issues">Diving deeper in Zone 3&rsquo;s latency issues.</h2>
<p>This performance discrepancy in Zone 3 was astonishing to us. According to Microsoft&rsquo;s best practices, the physical proximity of application servers to their respective databases within the same region should minimize latency. Our findings, however, painted a different picture, suggesting that within-region zone selection could profoundly impact application performance.</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/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/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>Our investigation was quite comprehensive, eliminating potential variables such as our own codebase, PHP environment configurations, MySQL performance, and much more. The consistent latency issues, particularly affecting PHP&rsquo;s single-threaded cron jobs, finally pointed us towards a more systemic issue within Zone 3&rsquo;s network architecture or deployment somewhere.</p>
<h2 id="collaboration-and-learning">Collaboration and learning</h2>
<p>Our journey, backed by the collaboration with <a href="https://www.pionative.com/" data-external>Pionative</a>, underscores the importance of partnership in navigating the cloud&rsquo;s complexity. As we dove deeper into this latency anomaly, our shared efforts aim not only to optimize our deployment but also to contribute our findings back to a broader Azure community.</p>
<blockquote class="pull-quote"><p>This latency impact is too large to ignore in our production environment.</p>
</blockquote>
<h2 id="implications-and-next-steps">Implications and next steps</h2>
<p>This anomaly in Zone 3 opens up discussions on optimal zone selection and deployment strategies within Azure&rsquo;s Western Europe region. Is this the reason that Zone 3 is the least populated zone? Online there isn’t much to find about this it looks like. So i wrote this post since this cost us literally days to pinpoint this performance difference.</p>
<blockquote>
<p>This will impact our production environment. We are going to run our DB in Zone 1 or 2. and the API must reside in either of these zones. Zone 3 can be used for pods that talk with the API.</p>
</blockquote>
<p>This latency anomaly in Zone 3 was more then a challenge for us. We took it as a opportunity to learn, adapt, and optimize our knowledge. As we continue to collaborate, test, and refine our approach, our journey into Azure is shaping up to be a very interesting technical exploration which is broadening our toolset for the future.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hotfixing with Docker Artifacts in a Git Flow Strategy</title>
      <link>https://www.allsubjectsmatter.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[Teams]]></category><category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[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.]]></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>This post is a follow up on GIT deployment strategies within Kubernetes environments. Make sure you read this post first:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.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/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/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 a CI/CD pipeline centered around Docker artifacts with the focus on a singular branch, implementing an efficient hotfix strategy requires a small deviation to ensure rapid deployment of critical fixes. Let&rsquo;s work out our approach to hotfixing in this Docker-centric environment, given the explained branching architecture in the above post.</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/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/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>Graphical reference of our branching and deployment strategy</figcaption></figure>

<h2 id="identifying-the-need-for-a-hotfix">Identifying the need for a Hotfix</h2>
<p>Hotfixes are a critical part of maintaining the stability and reliability of any software in production. They are typically required under specific circumstances that demand immediate attention. Understanding when to implement a hotfix is key to managing your CI/CD pipeline effectively, especially when using Docker artifacts and streamlined branching strategies like focusing primarily on a Single branch. Here are our scenarios where a hotfix can be necessary:</p>
<ol>
<li><strong>Security Vulnerabilities:</strong> One of the most urgent triggers for a hotfix is the discovery of a security vulnerability within your application. Whether it&rsquo;s an exposed data risk, a potential for unauthorized access, or any flaw that could be exploited maliciously, quick action is required to patch the vulnerability and protect your users and data.</li>
<li><strong>Critical Bugs Impacting User Experience:</strong> If a significant bug slips through your testing processes and reaches production, it can severely impact the user experience. This could be anything from a broken core functionality that prevents users from using your application as intended to severe performance issues leading to timeouts or crashes. If the issue significantly affects your user base, a hotfix is warranted.</li>
<li><strong>Compliance Issues:</strong> Certain applications may have legal or regulatory requirements. If a compliance issue is identified that could affect your legal standing or user privacy, addressing it swiftly through a hotfix becomes a priority to avoid penalties or breaches of trust.</li>
<li><strong>Data Loss Risks:</strong> Scenarios where there&rsquo;s a risk of data loss or corruption for users require immediate attention. Whether it&rsquo;s a flaw in how data is saved, updated, or retrieved, a hotfix should be deployed to mitigate any potential loss or corruption of user data.</li>
<li><strong>Payment or Transaction Failures:</strong> For applications dealing with payments or any form of transactions, issues that prevent users from completing transactions or that might lead to financial discrepancies are critical. Such problems not only affect user trust but can also have financial repercussions for both users and the service provider, necessitating a hotfix.</li>
</ol>
<h2 id="deploying-a-hotfix">Deploying a Hotfix</h2>
<p>When a critical issue is identified within the production environment, the immediate goal is to develop, test, and deploy a hotfix as quickly as possible. Given that our primary branch for development is MAIN and that we generate Docker artifacts for deployment, we need a process that allows for rapid intervention without disrupting the ongoing work.</p>
<ol>
<li><strong>Creating a Hotfix Branch</strong>: Instead of branching off from your Production branch (as LIVE is not represented anymore in the Git repository in this strategy), the hotfix branch is created from the commit currently deployed in production. This ensures that the Hotfix is based on the exact state of the version running on the production environment. The specific commit can be identified through tags or Docker image labels that correlate with the production deployment.</li>
<li><strong>Developing and Testing the Hotfix</strong>: The critical fix is developed within this hotfix branch. The urgency of the situation necessitates a focused approach, targeting the problem with minimal changes to expedite the testing and deployment process. Despite the rush, adherence to the Definition of Done (DoD) is crucial to ensure the stability and reliability of the hotfix. This can mean a condensed version of the DoD, prioritizing critical tests and checks that ensure the fix will not introduce new issues.</li>
<li><strong>Creating a Docker Artifact for the Hotfix</strong>: Once the hotfix meets the agreed DoD criteria, an automated process builds a Docker image from the hotfix branch. This Docker artifact is then tagged appropriately, indicating its purpose and urgency.</li>
<li><strong>Deploying the Hotfix</strong>: The newly created Docker artifact is deployed directly into the production environment, bypassing the usual DEV to STAGING to PRODUCTION environment strategy. This deployment is only justified by the critical nature of the fix and the testing it has undergone.</li>
<li><strong>Merging Back and Maintaining Consistency</strong>: After the hotfix is successfully running in production, it&rsquo;s essential to merge the changes back into the MAIN branch and any other relevant branches or tags. Only this step ensures that the fix is incorporated into the ongoing development work and future releases, preventing regression!!</li>
</ol>
<h2 id="ensuring-rapid-deployment-and-minimizing-disruption">Ensuring Rapid Deployment and Minimizing Disruption</h2>
<ul>
<li><strong>Automated Processes</strong>: Automation plays a key role in this strategy, from the creation of the Docker artifact to its deployment. Automated tests, builds, and deployments ensure that the hotfix process is as fast and efficient as possible.</li>
<li><strong>Communication and Monitoring</strong>: Rapid deployment of hotfixes requires clear communication among team members and close monitoring of the production environment post-deployment to ensure the fix is effective and does not introduce new issues.</li>
<li><strong>Documentation and Review</strong>: Documenting the hotfix process and conducting post-mortem reviews help in refining the hotfix strategy, ensuring that the team can respond even more effectively to future issues.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>In a CI/CD pipeline using Docker and primarily operating through Git Flow, hotfixing requires a streamlined approach that leverages automation and focuses on rapid, targeted fixes. By creating hotfix branches based on the production-deployed commit, developing and testing fixes according to an well defined DoD, and deploying Docker artifacts directly into production, teams can address critical issues swiftly while ensuring the ongoing stability and integrity of their application.</p>
]]></content:encoded>
    </item>
    <item>
      <title>CI/CD pipeline with Docker and Git branching strategy</title>
      <link>https://www.allsubjectsmatter.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[Teams]]></category><category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Exploring CI/CD with Docker &amp; Git. Mastering deploying Docker artifacts via Git branching. Dive into strategies for DEV, STAGING, PROD environments. Perfect your pipeline!]]></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>Let&rsquo;s dive into managing a CI/CD pipeline using Docker with a focus on Git branching strategy that involves development (DEV), staging (STAGING), and production (LIVE) branches. Our goal is to clarify the role each branch plays in the process, particularly how Docker artifacts are created and managed within this framework.</p>
<h2 id="docker-deployment-using-git-flow-with-multiple-branches">Docker deployment using Git Flow with multiple branches</h2>
<p>The core of our branching strategy involved three branches:</p>
<ol>
<li><strong>DEV</strong>: This is where all development work happens. Every new feature, bug fix, or enhancement starts in a feature branch, branched off from DEV. Once the work on a feature branch is complete, reviewed and meets the Definition of Done (DoD), it&rsquo;s merged back into DEV.</li>
<li><strong>STAGING</strong>: This branch mirrors a pre-production environment. It&rsquo;s where integrated features from DEV get tested as a whole and accepted by stakeholders to ensure they work together seamlessly.</li>
<li><strong>MASTER</strong>: The branch that reflects the current state of our live environment. Only thoroughly tested and stakeholder-approved changes from STAGING make it to Production.</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/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>This was our variation of a Git Flow strategy, we used until now. When you start working with Artifacts this all changes back to a normal Git Flow structure. We still have environments running (Dev, Staging and Prod). The real difference is that code commits get done once instead of getting merged into a branch running on Staging or in Production.</p>
<p>Instead we now have one Branch we called it Main. This is where a runnable artifact gets created after a successful code merge using Docker images. Everything deployed into our Staging or Production environment is a specific version of an earlier created Artifact.</p>
<p>The biggest change is that STAGING and PROD aren’t branches anymore where you merge code into. They have instead became environments that run a specific version of a Docker image. This all has its own challenges, especially when you need to Hotfix. Hotfixing has it’s own post which you can read after this post. Lets break down all of this without introducing hotfixing step by step first. It is comprehensive enough already.</p>
<h2 id="what-is-an-artifact">What is an Artifact?</h2>
<p>A Docker artifact, often referred to interchangeably with a Docker image, is a lightweight, standalone, executable package that includes everything needed to run a piece of software, including the code, a runtime, libraries, environment variables, and config files. In simple terms, it&rsquo;s like a snapshot of your application and its environment, bundled together, ready to be deployed and run anywhere Docker is installed, ensuring consistency across different environments.</p>
<h2 id="the-importance-of-dod-when-merging-code">The Importance of DoD When Merging code</h2>
<p>A comprehensive and clear Definition of Done (DoD) plays a pivotal role in this new process, even more when merging changes into a single branch that triggers a Docker image which should be runnable and deployable:</p>
<ul>
<li><strong>Quality Assurance</strong>: The DoD ensures that all changes, including new features, enhancements, and hotfixes merged back into MAIN, meet a consistent quality standard. This includes code quality checks, successful completion of automated tests, and adherence to security best practices.</li>
<li><strong>Preventing Regression</strong>: By applying the DoD to all merges into MAIN, the team can prevent regressions and ensure that new changes do not negatively affect existing functionality. This is crucial for maintaining the integrity and stability of the codebase.</li>
<li><strong>Streamlining Deployments</strong>: A clear and comprehensive DoD facilitates smoother deployments by ensuring that only fully vetted and ready-to-deploy changes are merged into MAIN. This reduces the risk of wasting resources on building Docker images that are bugged or broken, and therefore contributes to more efficient and reliable artifacts.</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/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/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>Here a graphical presentation of our Git Flow strategy and how this flows trough the different environments we have.</figcaption></figure>

<h2 id="creating-and-managing-artifacts">Creating and Managing Artifacts</h2>
<p>The first thing for this proces to work is when a forked branch is merged back into MAIN, this triggers an automated process in our CI/CD pipeline. Here&rsquo;s what happens:</p>
<ol>
<li><strong>Automated Testing</strong>: First, automated tests run to ensure the new changes meet our quality standards. This is crucial for maintaining a stable branch.</li>
<li><strong>Docker Artifact Creation</strong>: Upon successful running al tests, a Docker image (our Artifact) is created from the MAIN branch. This is because the MAIN branch is the single branch that represents the latest state of every DONE development, This is where we generate our artifacts from that can become “shippable” products.</li>
<li><strong>Why using MAIN for Docker Artifacts?</strong>: Creating artifacts at this stage allows us to catch any integration issue. Since every feature merged into MAIN is going to be considered ready for a release, generating an artifact here ensures that we always have a tested and potentially deployable version of our product at hand.</li>
</ol>
<h2 id="transferring-from-branches-to-environments">Transferring from Branches to Environments</h2>
<p>In traditional Git workflows, branches like STAGING and PROD are used to represent different stages of the development process, from testing to production. However, in a Docker-centric CI/CD pipeline, the focus shifts from using these branches for code integration to using them as references for deployment environments.</p>
<h2 id="deploying-to-staging">Deploying to STAGING</h2>
<ul>
<li><strong>Creating Docker Artifacts</strong>: Once development is complete and merged into the MAIN branch, a Docker artifact is automatically created. This artifact is a snapshot of your application at that point in time, encapsulating all the changes that were merged.</li>
<li><strong>Deployment, Not Merging</strong>: The STAGING environment&rsquo;s role is to test this Docker artifact in conditions that closely mimic the production environment. Instead of merging code from MAIN to a STAGING branch, you deploy the Docker artifact to a STAGING environment. This means the code itself isn&rsquo;t moving between branches; rather, the compiled and containerized version of your application is being tested.</li>
<li><strong>Purpose of STAGING</strong>: The STAGING environment is essential for comprehensive testing, including performance and security assessments. It&rsquo;s a final check to ensure everything works as expected in a production-like setting without directly affecting your actual users.</li>
</ul>
<h2 id="going-to-production">Going to Production</h2>
<ul>
<li><strong>Stakeholder Approval</strong>: Once your Docker artifact has been rigorously tested in STAGING and approved by stakeholders, it&rsquo;s ready to move to production. This step is crucial for ensuring that all features, fixes, and updates meet the necessary standards and expectations.</li>
<li><strong>Deployment to Production</strong>: The transition from STAGING to PROD involves deploying the tested Docker artifact to your production environment. This deployment process ensures consistency, as the exact artifact tested in STAGING is what gets deployed to PROD, eliminating any discrepancies between tested and deployed code.</li>
</ul>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li><strong>No More Branch Merging for Deployment</strong>: In this streamlined workflow, STAGING and PROD branches are no longer used for merging code. Instead, they reference specific deployment environments where Docker artifacts are deployed for testing and production.</li>
<li><strong>Focus on Artifacts</strong>: The emphasis is on creating, testing, and deploying Docker artifacts. These artifacts represent stable versions of your application ready for STAGING and, eventually, PROD deployment.</li>
<li><strong>Consistency and Reliability</strong>: By deploying the same Docker artifact from STAGING to PROD, you ensure that what has been tested is exactly what gets released, increasing the reliability and predictability of your deployments.</li>
</ul>
<p>This approach simplifies the deployment process, reduces the risk of errors, and ensures that every release is thoroughly tested in an environment that mirrors production as closely as possible.</p>
<h2 id="are-multiple-branches-still-needed">Are multiple Branches still needed?</h2>
<p>Given this new workflow, the STAGING and PROD branches in Git branching serve no active purpose anymore and therefore they have became obsolete. They helped track which versions of the code have been deployed to each environment which we have solved by running a specific version of an Artifact.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Incorporating Docker into our Git branching strategy streamlines the development-to-deployment process by ensuring that every merge into MAIN triggers a Docker image with the potential to go to production. The key to this strategy is the creation of Docker artifacts early in the development cycle, allowing for continuous integration and delivery while maintaining high standards of quality and stability.</p>
<p>By understanding the changes to Git branches and the process of creating and deploying Docker artifacts, we can rely on the complexities of CI/CD with confidence, ensuring that all new features and fixes are delivered to users as efficiently and reliably as possible.</p>
<h2 id="hotfixing">Hotfixing</h2>
<p>We havent spoken about hotfixing yet. As i said this deserves its own Post. You can read all about Hotfixing and its caveats here.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.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/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/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>Why &#34;AI&#34; caught you by surprise</title>
      <link>https://www.allsubjectsmatter.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[Strategy]]></category>
      <description>&lt;![CDATA[Explore the AI (r)evolution. Discover how intent understanding transformed AI, from Flow.ai to ChatGPT, transforming the digital world in steps. Dive in for my own unique insights!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/why-ai-caught-you-by-surprise/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>As someone interested in the ever-evolving world of technology, I’ve witnessed a lot of developments in AI, particularly in the realms of language processing and user interaction. Reflecting on these advancements, I&rsquo;ve been wanting to share why, in my opinion, the recent strides in AI aren&rsquo;t as surprising as you might find them to be.</p>
<p>I use different pronounces as i refer to the same thing. I use AI and LLM (Large Language Model) for todays take on systems like ChatGPT.</p>
<h2 id="my-first-encounters-with-ai">My first Encounters with &ldquo;AI&rdquo;</h2>
<p>My journey with LLMs began earlier than some people realize. I had my first experiences with Flow.ai around 2018/2019. Back then, it was clear to me that we were on the brink of something. However, the general public seemed largely unaware of these undercurrents in AI development. The fact is, companies like Flow.ai (now part of Khoros) were laying the foundation for AI systems we see today like ChatGPT.</p>
<p>Read up on a post i wrote back in 2019 about our experiences with Flow.AI together with Stefan Samba.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.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/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/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: A Missed Opportunity for 333travel</h2>
<p>My experience with Flow.ai was an eye-opener back then. It was a game-changer in the AI space for me, demonstrating the potential of neural network-based conversational AI. Looking back at it, when we tried integrating this technology at 333travel back in 2018, we might have been to far ahead of our time. Our natural style of helping people with asking for a proposal, did not gain the traction we hoped for using a LLM. Our customers (luckily?) preferred interacting with human specialists rather than bots. This was a clear indication that while the technology was promising, market readiness was another story.</p>
<blockquote class="pull-quote"><p>People hated talking to the &ldquo;oldstyle&rdquo; chatbots not understanding what you meant due to the lack of intent understanding.</p>
</blockquote>
<h2 id="the-siri-example">The Siri example</h2>
<p>Let&rsquo;s talk a bit about Siri, Apple&rsquo;s virtual assistant. Since its launch in 2011, Siri has made some strides. However, from my point of view, these improvements have been modest at best in the intent learning space. Siri, along with other tools like Cortana and Google Assistant, still required very specific commands and often struggled with understanding intent. While there has been some progress there, it&rsquo;s been to slow of a curve in understanding human language and mainly intent.</p>
<ul>
<li><strong>2010-2011</strong>: Siri&rsquo;s initial years saw its integration into the iPhone 4S, offering basic voice recognition and command execution.</li>
<li><strong>2012-2013</strong>: Expansion into new languages and dialects, broadening its user base.</li>
<li><strong>2014-2015</strong>: Entry into new markets and the launch of SiriKit, allowing third-party app integration.</li>
<li><strong>2016-2018</strong>: Introduction to macOS and the release of Shortcuts for customized voice commands.</li>
<li><strong>2020-2021</strong>: moderate improvements in language translation, integration with Apple&rsquo;s ecosystem, and enhanced speech recognition.</li>
</ul>
<h2 id="ai-and-customer-interaction">AI and Customer Interaction</h2>
<p>As a technology enthusiast, I find ChatGPT impressive in many ways, but it&rsquo;s essential to acknowledge that it&rsquo;s no substitute for the detailed understanding that human specialists provide. Lets take a Tour Operator for example. Crafting a feasible travel plan is a whole different take. The last thing you want is to make mistakes there.</p>
<p>So why would we let an AI do the talking to the customer and then replace it with a human? Isn&rsquo;t it better to do it the other way around? Let our specialists use an AI to create a personal travel plan and send it quicker using AI? Bonding your customer with real human interaction is most preferable when doing sales.</p>
<blockquote>
<p>If you dont know what you are talking about you are not able to spot mistakes in responses from AI. This will always be a major concern. First hand knowledge is still mandatory. That is why something like ChatGPT is a tool that needs to be used by skilled people about the subject to accelerate their work.</p>
</blockquote>
<p>I dont believe in separate integrations within websites that take up space pretending to be a smart AI. The search function already present on websites will solve this issue. and people that are going to use an AI (LLM) dont want to interact with a different LLM every time. they will use their own PA to answer these questions if needed.</p>
<blockquote>
<p>The first step companies need to solve is implementing a search engine in their website that understands intent. By understanding intent you can give people actually a useful answer based on the content on your website.</p>
</blockquote>
<p>You need a profound integration with your LLM and your rich content. Especially in the Travel Industry. Using your website to become smarter using LLM is much more logical in my opinion then a popup &ldquo;person&rdquo; you can talk to.</p>
<blockquote class="pull-quote"><p>The future of AI is undoubtedly exciting, but it&rsquo;s a path we must navigate with both optimism and caution.</p>
</blockquote>
<h2 id="my-take-on-ais-future">My Take on AI&rsquo;s Future</h2>
<p>Looking ahead, I see AI as a field growing with possibilities but also filled with challenges. The journey from the early days of specific command-based systems to the more intuitive, context-aware models from today have been fascinating. However, there is still a long way to go in achieving the of AI that can really understand and interact with humans on a more profound level.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In summary, while AI&rsquo;s recent advancements may seem sudden and surprising to many, they are part of a long, ongoing process of technological evolution. My experiences at 333travel, especially with innovative platforms like Flow.ai, have shown me the potential and limitations of AI. As we move forward, I believe it&rsquo;s crucial to balance our enthusiasm for AI with a realistic understanding of its current capabilities and limitations.</p>
<blockquote class="pull-quote"><p>You are unique by keeping it close to yourself and not by using technology to do your core business for you. Always keep a personal touch!</p>
</blockquote>
]]></content:encoded>
    </item>
  </channel>
</rss>
