<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://blog.maikel.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.maikel.dev/" rel="alternate" type="text/html" /><updated>2026-05-02T17:15:36+00:00</updated><id>https://blog.maikel.dev/feed.xml</id><title type="html">Maikelology</title><subtitle>Ramblings of @maikel@vmst.io</subtitle><author><name>Maikel Frias Mosquea</name></author><entry><title type="html">The AI Bubble Is Bursting</title><link href="https://blog.maikel.dev/2026/05/01/the-ai-bubble-is-bursting.html" rel="alternate" type="text/html" title="The AI Bubble Is Bursting" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://blog.maikel.dev/2026/05/01/the-ai-bubble-is-bursting</id><content type="html" xml:base="https://blog.maikel.dev/2026/05/01/the-ai-bubble-is-bursting.html"><![CDATA[<p>I’m hoping by the end of this article, addressed first and foremost to software engineers, you feel a bit more hopeful about your job security. I want to talk about what millions of euros on PR have ended up forcing us to call AI (and is not), and why I think the interesting shift isn’t “how big is the model,” it’s the <strong>harness</strong>, the tools and retrieval and guardrails and memory that turn a small language model into something actually useful. But first you need to know what’s my angle on this.</p>

<p>These are my questions about it</p>

<ol>
  <li>What can it actually do <strong>today</strong> for me?</li>
  <li>How can it help me or make me worse at my job.</li>
  <li>How could it disrupt the economy or cost us our jobs.</li>
</ol>

<p>I wouldn’t say I’m an early adopter, but close enough considering how much time and energy I’ve invested in understanding what all this noise is about. In this article I’m going to explain why I think <a href="https://openai.com">OpenAI</a> and <a href="https://anthropic.com">Anthropic</a> are in trouble, they know they are, and all the money they are spending is mostly on pretending they aren’t worthless. By the end I hope you feel better about job security not because AI is useless, but because what’s replacing the hype is stuff <strong>you</strong> can build and control.</p>

<h2 id="what-an-llm-actually-is">What an LLM actually is</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1736248991839-67d11f7a5643?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMzQ5fA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="Close-up of newspaper columns, prior written language that models extrapolate from" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/a-close-up-of-a-newspaper-with-writing-on-it-0wELZlsVDn4" target="_blank" rel="noopener noreferrer">Close-up of newspaper columns, prior written language that models extrapolate from by Martin Zenker</a>
  </figcaption>
  
</figure>

<p>All texts they generate are based on <strong>pre</strong>-<strong>existing</strong> text they’ve been fed, they look at those texts, create tables of resemblance, and figure what the next word might be based on statistics. These “words” units are called tokens and they aren’t necessarily words, they can be subdivisions of a word. So a modern chat model is a <strong>next-token predictor</strong>. It doesn’t care about truth, just probability of wording. It <strong>statistically continues</strong> patterns that looked plausible in training.</p>

<p>People call this a <strong>stochastic parrot</strong>. They just statistically predict and repeat patterns from their training data, mimicking human-like responses without real comprehension or intent.</p>

<p>When you enable thinking mode on some models all it does is use more text and fill the context window quicker. It can spend ten minutes discussing with itself whether its cutoff is 2024 or 2026. Every new line gives it more text to generate the next line. So it’s a glorified search engine where facts and accuracy don’t matter.</p>

<p>The weights don’t <strong>learn</strong> your project overnight. Anything that feels like memory or fresh facts usually comes from <strong>context you fed</strong>, <strong>tools</strong>, or <strong>external stores</strong>, not from the model silently updating itself.</p>

<h2 id="what-can-ai-do-today">What can AI do TODAY?</h2>

<p>Briefly:</p>

<ol>
  <li>They can take questions written in natural language, even with some typos, and reply back with what looks like, increasingly less with every new version, human written text.</li>
  <li>They can also generate images, sounds and video.</li>
  <li>They can write code for multiple languages but specially Python and Javascript.</li>
  <li>They can fool people into accepting as truths something that is just made up text.</li>
  <li>They are all sycophants aiming to be agreeable or fake being right, none of them can distinguish truth from lie.</li>
</ol>

<p>I’m going to focus on the ones that matter to me.</p>

<h2 id="where-todays-stack-fails">Where today’s stack fails</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1766170507900-a337b5b27cae?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAxfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="A tangled mass of cables, suggesting technical debt, limits, and friction" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/tangled-mass-of-electrical-wires-on-pole-8dHGPxPKOB0" target="_blank" rel="noopener noreferrer">A tangled mass of cables, suggesting technical debt, limits, and friction by Bernd 📷 Dittrich</a>
  </figcaption>
  
</figure>

<p>For me it breaks down into three kinds of problems: technical limits, economic structure, and product trust.</p>

<h3 id="technical-limits">Technical limits</h3>

<p>They cannot make something out of nothing. Everything they spit out comes from what they were trained on plus whatever prompt you hand them. Ask it to generate a frontend and you’ll usually get something generic and boring that you could do quicker in <a href="https://bootstrapstudio.io">Bootstrap Studio</a>. You’ll probably waste more time trying to fine tune the prompt to get what you want than if you’d just built it yourself.</p>

<p>Not to mention the royal pain in the arse of finding out whether the code you get is comes from others with licences that clash with your product (especially viral ones), given LLMs don’t spare a thought for the origins of any of their training data.</p>

<p>Ask the model its cutoff date and work backwards. Take Elixir as a good example. Most models I can download know nothing past version <strong>1.16</strong>. Ask your model what it thinks of <code class="language-plaintext highlighter-rouge">unless</code> and if it believes it is deprecated. Most will fight you.</p>

<p>Ever growing models add absolutely nothing to software engineers using them if they are bad at coding in their language of choice. The languages they write “the best” are often flimsy Javascript (hundreds of versions, lots of low quality JS online) and Python (still fragmented on packaging, still carrying 2.x vs 3.x baggage). Try <a href="https://www.jetbrains.com/kotlin-multiplatform/">Kotlin Multiplatform</a> or Elixir despite the existence of centralised <a href="https://hexdocs.pm">Hexdocs</a> and you still hit outdated code and silly mistakes.</p>

<p><strong>Hallucinations</strong> are the model filling in instead of saying I don’t know, unless the <strong>harness</strong> stops it or grounds it with retrieval and citations.</p>

<p>So people bolt on <strong>RAG</strong> (retrieval-augmented generation, fetch relevant chunks then generate) or do a bit of fine tuning. You do <strong>not</strong> need OpenAI’s or Anthropic’s latest flagship for this. <strong>Nope.</strong> Another sign that the real product isn’t just the raw weights. And, crucially, there’s no magic button they can press to conjure up a GPT 7.0 or a Claude Popsicle that’s up to date with everything on the internet right now. The best they can offer is always a snapshot, always some version behind reality.</p>

<h3 id="economic-and-structural-pressures">Economic and structural pressures</h3>

<p>The most costly part of LLMs is not inference, it’s creating the model. <strong>Vast</strong> data, ever larger datacentres, chips, water, <strong>financial resources</strong>. And even then you get cutoff dates. Its knowledge is always frozen in time.</p>

<p>Oldest trick in the book, on Facebook YOU are the product, in Google your searches are the product. We never left the web 2.0 era. Your prompts are content. The pitch for chat.insertyourproviderofchoice.com is width of knowledge so you’ll type more about your life. Anyone on the receiving end can use that to push better ads on you, or worse.</p>

<h3 id="fediverse-vs-twitter">Fediverse vs Twitter</h3>

<p>You have seen this pattern before. One company owns the megaphone, the feed, the rules, and the ad layer. <a href="https://joinmastodon.org">Mastodon</a> and the wider <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a> are the opposite bet. Many servers, many communities, you can move instances, you can follow people across servers, and nobody in the middle is trying to turn your whole social graph into ad inventory by default. <strong>Twitter</strong> is still where people go for <strong>reach</strong> and <strong>one global scoreboard</strong>. The fediverse trades some of that discoverability for <strong>exit</strong> and <strong>community rulebooks you can actually read</strong>. Neither side is perfect. The point for this article is structural. <strong>Who runs the pipe, and who gets to read the stream.</strong> A glossy cloud chat product is Twitter in LLM form. A local model with your own RAG, MCP stack, and guardrails is closer to picking your own instance and owning more of the plumbing you even get to choose Mastodon, Pleroma, Akkoma, Sharkey, GoToSocial.</p>

<p>If the model can reach tools, search, APIs, <a href="https://modelcontextprotocol.io"><strong>MCP</strong></a> servers (Model Context Protocol, a standard way for assistants to call external tools and data), sheer memorised bulk matters less. <a href="https://chat.mistral.ai">Mistral Le Chat</a> has stuff like searching <a href="https://pubmed.ncbi.nlm.nih.gov">PubMed</a> and linking claims. A small model that <strong>queries</strong> sources beats a giant one that <strong>guesses</strong> citations. Tools turn the LLM into a front end for search and structured sources, often more accurate than a 500B-parameter memory of the internet that’s still wrong when it’s released.</p>

<h3 id="product-fatigue-and-trust">Product fatigue and trust</h3>

<p>They are so pervasive universities barely need <a href="https://turnitin.com">Turnitin</a>. Identifying AI prose is easy, think of… instead of…, em-dashes everywhere, first paragraph praises you, last paragraph open-ended so you burn tokens. Online communities notice. On <a href="https://reddit.com">Reddit</a> people call it out so fast it’s become a sport. It’s a bit like the SouthPark episode where the kids were obsessed with a game until their parents liked it and the novelty vanished.</p>

<p>For search, every time you use Google you get AI generated crap at the top where half the time the summary contradicts the links. If that annoys me, imagine everyone else. I lean harder on <a href="https://duckduckgo.com">DuckDuckGo</a> even though they also do AI but it’s far less intrusive the more Google forces me to read <a href="https://gemini.google.com">Gemini</a> responses before the actual results I am looking for. I can even achieve more accurate results using a simple MCP in <a href="https://lmstudio.ai">LMStudio</a> that uses DuckDuckGo and then fetches the results to give me a properly accurate answer.</p>

<h2 id="enter-the-room-what-makes-a-model-interesting-the-harness">Enter the room what makes a model interesting, <strong>the harness</strong></h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1667494398891-dd00bad6e8d8?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAxfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="A vast server hall, infrastructure and harness at scale" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/a-person-walking-in-a-large-room-97VU2DS3hHE" target="_blank" rel="noopener noreferrer">A vast server hall, infrastructure and harness at scale by Yoan</a>
  </figcaption>
  
</figure>

<p>I hate this word but have to use it to explain the immediate future.</p>

<p>Everything they are good at does not require renting the biggest proprietary model anymore when you get the harness right.</p>

<p>A harness is just a bunch of tools that combined make a model actually useful. You could say <a href="https://cursor.com">Cursor IDE</a> is a harness, <a href="https://zed.dev">Zed Editor</a> is a harness, <a href="https://github.com/features/copilot">Copilot</a>, <a href="https://claude.com/claude-code">Claude Code</a>, <a href="https://pi.dev">Pi dev</a>, <a href="https://roocode.com">RooCode</a>/ZooCode are all harnesses. Imagine the LLM as plutonium. The harness is the whole plant around it. The same kind of hot reactive core can either be the payload in a delivery system or fuel in a reactor hall. One story ends in a flash everyone remembers for the wrong reasons. The other is just containment, coolant, control rods, years of boring maintenance, and what you get out of it is power you can actually use, lights on, turbines turning, work getting done. Unless Homer Simpson is in charge of security.</p>

<p>They usually include a mixture of:</p>

<ol>
  <li><strong>RAG</strong>: To some extent navigate your code to find where the lines are that perform some function, then depending on your question, feed those lines to the model prepending your prompt. In other words they add context the model wouldn’t be aware of otherwise. They can also navigate and catalogue external sources like documentation.</li>
  <li><strong>Guardrails</strong>: they define what the model is allowed to do with tools that need user privileges, from creating, editing or deleting files to accessing the internet or running commands in the shell. Rules and limits, not the thing that runs them.</li>
  <li><strong>MCPs</strong>: extra functionality that adds intelligence to the harness. They can allow the LLM to access your <a href="https://obsidian.md">Obsidian</a> notes, get the latest docs on some language, or control a browser.</li>
  <li><strong>A compactor</strong>: something that summarises the session, usually in an internal markdown file, that later is fed into a new session so the model maintains some memory of what it was doing. Because all models have context window limits and they are a lot shorter than our working memory.</li>
  <li><strong>An Agent</strong>: the part you actually interface with, chat surface, diffs, approve or reject, terminal output, all of it. The model outputs text, the <strong>agent</strong> is what turns that into real tool calls and runs them <strong>inside</strong> those guardrails from item 2. It is the loop that proposes an edit or a command, checks if it’s allowed, and executes or asks you, then feeds results back to the model. Without that agent you’re back to a plain chat window that can’t drive your IDE or your shell.</li>
</ol>

<p>The two most important jobs of the harness are to cut hallucinations and to supply memory outside the context window. Both goals are interconnected.</p>

<blockquote>
  <p><strong>Models do not learn</strong></p>
</blockquote>

<p>If you plug a tiny model like <a href="https://ai.google.dev/gemma">Gemma</a> to an MCP that gives it memory you can tell it from LMStudio to remember your name, some crucial data you have that will be useful, and Gemma won’t remember any of it BUT will use the MCP tool you’re using for long-term memory to store that data, which will ultimately land in a vector database like <strong><a href="https://qdrant.tech">Qdrant</a></strong>. Then when you ask “do you remember what’s my name” Gemma pulls it from your MCP server and answers correctly. It feels as if the model remembers but it actually does not, it just has external memory.</p>

<p>So the point is, a modest model plus retrieval and tools often beats a massive frozen generalist for fresh, checkable answers. Same for code when the harness grounds work in repo and docs.</p>

<h2 id="so-how-does-this-affect-openai-and-anthropic">So how does this affect OpenAI and Anthropic</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1770734360042-676ef707d022?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAyfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="Lines of code on a monitor, the rent-the-cloud pitch versus real engineering" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/computer-screen-displaying-lines-of-code-5sLNGV2EFRM" target="_blank" rel="noopener noreferrer">Lines of code on a monitor, the rent-the-cloud pitch versus real engineering by Harshit Katiyar</a>
  </figcaption>
  
</figure>

<p>If their core sell is omniscience baked into the weights, but users including younger generations are tired of <strong>very wrong</strong> and verbose answers, and small local models with MCPs and RAG can answer faster and more accurately for real work, then having the biggest model isn’t what locks people in anymore. What’s left is convenience, habit, distribution.</p>

<p>But now for that convenience you pay ever increasing token costs. <strong>Dario Amodei</strong> is telling you to use multiple agents in parallel to get a better answer (sure, burn more tokens, make him richer, what would he say, use Claude less?). Software engineers are getting fed up with how expensive every hallucination is and how verbose and inaccurate generated code is getting.</p>

<p>Meanwhile early adopters running local LLMs like me have reached a point where downloadable LLMs plus custom MCPs work <strong>better</strong> for me than commercial cloud ones, and they don’t suck my personal data since it runs on our PCs. We’re past the peak where you needed two H100s to run a decent model. I have my tailor-made local PRIVATE stuff on my puny <strong>5060 Ti</strong> with <strong>16GB</strong> VRAM. I belong to two of their target demographics, software engineer AND early adopter, and I don’t need them. Imagine once the rest realises the same.</p>

<p><strong>Enterprise reality check:</strong> cloud APIs only matter when the organisation <strong>refuses to self-host</strong>, full stop. Because, no matter how good their sales pitch is, a lot of companies simply <strong>can’t</strong> ship their code, their customer data, their internals through someone else’s API and sleep at night. Contracts, GDPR, NDAs, basic sense of risk, there are many reasons not to. Privacy isn’t a nice-to-have for most workplaces, it’s a hard constraint. So I’m saying value moves to harnesses and specialisation <strong>and</strong> to setups that stay on your own metal when the cloud is legally off limits.</p>

<p>The trend is clear. OpenAI and Anthropic aren’t just competing against each other but against how people actually want to use these systems. Their advantage was never really parameter count, only the <strong>perception</strong> that bigger meant better. That perception is fading among engineers and hobbyists, and regular people are tired of AI shoved into every surface. <strong>So the focus has shifted to maintaining the illusion through PR and hype.</strong></p>

<p>Look at Mythos on the Linux kernel for how that plays out. You get the news cycle and pieces like https://www.darkreading.com/vulnerabilities-threats/ai-assisted-software-scan-linux-bug, big scary <strong>AI found bugs in the kernel</strong> energy, then you dig into what actually matters for real threat models and half the punchline is <strong>local access</strong>, <strong>physical access</strong>, you already have to be at the machine in ways that make the headline feel ridiculous for anyone trying to prioritise actual risk. If I can touch the box I can do worse than whatever CVE paragraph fourteen buried.</p>

<p>The Reddit thread https://www.reddit.com/r/linux/comments/1sk9hcd/how_linux_plan_to_patch_the_exploits_discovered/ is worth reading for tone. People aren’t stupid. There’s a comment that says the quiet part out loud, this smells like an advertising campaign, scare users then sell the same brand as the thing that finds <strong>and</strong> patches the holes, <strong>subscribe now</strong>.</p>

<p><strong>Basically you don’t need them.</strong> Those misnamed systems aren’t AI in the sci-fi sense, they’re bloated natural language processors (NLP), and none of them will ever be close to artificial general intelligence.</p>

<h2 id="what-is-ai-actually-good-for-as-a-coder">What is AI actually good for as a coder</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1649451844924-0d7a218e02c9?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAyfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="A laptop on a desk, the everyday grind of boilerplate and craft" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/a-laptop-computer-sitting-on-top-of-a-desk-EyAhn0QBpR8" target="_blank" rel="noopener noreferrer">A laptop on a desk, the everyday grind of boilerplate and craft by Bernd 📷 Dittrich</a>
  </figcaption>
  
</figure>

<h3 id="boilerplate-code">Boilerplate code</h3>

<p>You want to write in TDD style some new feature, you need fixtures, sandboxes, test doubles and mockups. THE BORING stuff. That’s where it shines and you still need to check those fixtures match reality, let alone the latest features of the language for testing.</p>

<h3 id="aided-summarisation-and-aided-article">Aided summarisation and aided article</h3>

<p>If you write a very long post AI could summarise it for you but it’ll take all the personality out of it and sound bland and boring, hence why I wrote AIDED summarisation and not summarisation, aided article writing and not article writing. It’s good to give them your article and tell it to suggest a better structure of the headers after you’ve written it. The LLM will figure normally what is it that you’re trying to convey and guide you a better order of headers so you keep your voice, your tone, your typing, your million of typos (cough cough) BUT it is a lot easier to read now for others since now with the improved order, it is harder to lose track. This is specially useful for neurodivergent people (hi, that’s me and a huge percentage of coders) since we usually don’t think linearly.</p>

<h3 id="translation">Translation</h3>

<p>Obvious one. Easiest one for them to accomplish. Do we need 500B parametres models for this? Nope.</p>

<h3 id="shaping-ideas-or-feasibility-brainstorming">Shaping ideas or feasibility, brainstorming</h3>

<p>I wrote another post about using AI to check the feasibility of an idea. It’ll definitely write awful code but you often just want to brainstorm how possible it is to develop something and what problems you might face. The creativity is all yours and you compose prompt by prompt something you’ll never push to production, resembling your idea enough for you to figure the problems you’ll find in the way and the extras you’ll need to design. Let alone it allows you to reach full completion of your idea before actually starting to work on it, no SCRUM needed, no Agile, your focus is to write a full requirements document before you write your first line of code.</p>

<p>I strongly suggest using a different language than your target one if you’re serious about the idea, on one side that avoids licensing issues and on the other side that forces you to write the code yourself on your target language.</p>

<h2 id="what-i-think-everybody-should-do">What I think everybody should do</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1754663892533-6fdb748f25ad?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAyfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="A curved monitor and laptop on a desk, a space you own and configure" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/curved-monitor-and-laptop-on-a-desk-h8Ot-KfpHVs" target="_blank" rel="noopener noreferrer">A curved monitor and laptop on a desk, a space you own and configure by Jesus Jimenez Mora</a>
  </figcaption>
  
</figure>

<h3 id="1-stop-supporting-the-worst-business-model">1. Stop supporting the worst business model</h3>

<p>Let the Scam Allmens of Sillicon Valley fall on the weight of their own predictions. When you use cloud-based commercial LLMs you’re paying to train the next model on how much they can model your soul from your prompts, supporting infrastructure that’s destroying the planet, and giving data that can target you for ads <strong>or worse</strong>. People willingly write mental health stuff into chats, and even chat about their life-threatening alleries like they’re handing over a playbook on how to destroy them.</p>

<p>Switch instead to local models. Both <strong><a href="https://ollama.com">Ollama</a></strong> and <strong>LMStudio</strong> use <a href="https://github.com/ggml-org/llama.cpp">Llama.cpp</a> behind the scenes, you can even use it directly and it is supposedly faster. I prefer LMStudio because I like the interface more and token generation per second is high enough (anything from 20 or more is good enough) that faster wouldn’t make a difference for me reading or reviewing. I tend to use <strong><a href="https://qwen.ai">Qwen 2.5 coder 14B</a></strong>, <strong><a href="https://mistral.ai/news/devstral">Mistral Devstral</a></strong>, <strong>Qwen3.6 9B</strong>, and <strong>Gemma</strong>. For conversation with internet the 9B models are good enough. For coding it’s got to be a “coder” version of Qwen or Devstral.</p>

<p>I have an Nvidia <strong>5060</strong> with just <strong>16GB</strong> VRAM and so far that’s enough. I’ve tried larger models on <a href="https://thundercompute.com">ThunderCompute</a>, <a href="https://runpod.io">Runpod</a> and others and sure the code seemed better and tokens per second were higher, but they just seemed, on a closer look I couldn’t keep up with it. So there’s no point in a model typing faster if every time you review the code there’s too much of it. Again my right&gt; -arm tattoo is right:</p>

<blockquote>
  <p>LESS IS MORE</p>
</blockquote>

<p>When I stopped chasing bigger models on rented cloud PCs with H100 GPUs, I focused on long-term memory with ingestion of self-updating data and moved into RAGs, vector DBs, and <strong><a href="https://oban.pro">Oban</a></strong> to self-update. I had the pleasure of being at <strong><a href="https://www.elixirconf.eu">ElixirConfEU</a></strong> and unknown to me had the creator of <strong><a href="https://hex.pm">hex.pm</a></strong>, <strong>hexdocs</strong> and <strong><a href="https://hexdocs.pm/ecto">Ecto</a></strong> by my side, I went in without googling anyone on purpose so I get to know people before titles. I learnt that unlike everything in <a href="https://codeberg.org">codeberg.org</a>, everything in hexdocs can be scraped as needed, and all docs of deps sit in <strong>deps/</strong> too, so you avoid scraping unless you want your RAG to check for a new stable version that doesn’t match what’s on your elixir project’s folder.</p>

<h3 id="local-llms-vs-cloud-commercial-apis">Local LLMs vs cloud commercial APIs</h3>

<p>Same story as above, different stack. Quick contrast before we go deeper on harnesses.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th><strong>Local</strong> (your PC, or a VPC you control)</th>
      <th><strong>Cloud commercial</strong> (their API, their chat UI)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Where the sensitive stuff goes</strong></td>
      <td>Stays inside your trust boundary if you build it that way</td>
      <td>Prompts and outputs cross their infrastructure</td>
    </tr>
    <tr>
      <td><strong>What you pay with</strong></td>
      <td>GPU, electricity, time wiring MCP, RAG, vector DBs</td>
      <td>Tokens, changing prices, usage caps, surprise invoices</td>
    </tr>
    <tr>
      <td><strong>Top-end capability</strong></td>
      <td>Capped by VRAM and what you are willing to run</td>
      <td>They can point you at the biggest model they sell</td>
    </tr>
    <tr>
      <td><strong>Harness</strong></td>
      <td>You choose or build retrieval, tools, rules</td>
      <td>Often turnkey, sometimes you cannot see half of what it does</td>
    </tr>
  </tbody>
</table>

<p><strong>Both OpenAI and Anthropic</strong> share the same pitch: rent the flagship and hope the weights know your repo, and the table above shows where the value actually is (memory, tools, retrieval, who owns your prompts). <strong>The product was never only the weights.</strong> Once that lands, section 2 below is the practical half. Anthropic goes even further and is trying to convince you the solution is to use multiple agents burning tokens in parallel to balance each other. That’s efficiency at making them richer, not you getting solutions today.</p>

<h3 id="2-develop-your-own-harness">2. Develop your own harness</h3>

<p>I first noticed the RAG thing thanks to <strong>RooCode</strong>, indexing into local <strong>Qdrant</strong> with a local embedder and your whole repo locally, so you can ask “where’s the function that creates a new nix environment on my Fish shell folder” instead of “where’s create_nix” and it answers. So I thought:</p>

<blockquote>
  <p>…wait, my puny local PC is able to answer questions about the whole of the codebase by meaning?</p>
</blockquote>

<p>Then I thought what if my entire Obsidian Vault, what if the latest Kotlin Multiplatform docs, what if I inject <a href="https://www.rust-lang.org">Rust</a> docs in LMStudio to finally have me explain <strong>in a way that it sticks</strong> how the borrow checker works.</p>

<p>I coded a preprocessor on LMStudio but preprocessors only work on LMStudio (meaning their UI), so I converted it into an MCP in Elixir called <strong>remember_ex</strong> and kept fiddling with it. I can inject ePub docs and PDFs into Qdrant now. Chunking is still the hard part.</p>

<p>Eventually at <strong>ElixirConfEU</strong> <strong>George Guimaraes</strong> was presenting <strong><a href="https://github.com/georgeguimaraes/arcana">Arcana</a></strong>, I haven’t tried it yet but I will. Surprisingly we were doing very similar things and reaching similar conclusions, like using another LLM to chunk the texts better, <strong>Oban</strong> in the background so retrieval isn’t blocked by pending chunking. I still need to check heavy jobs don’t block LMStudio and supposedly when I run a model I have 4 threads but I’m not entirely sure that’s true or that my GPU can actually handle 4 at the same time. So we’ll see. That’s my path, let’s focus now on yours.</p>

<p>What should you try? Start small.</p>

<ul>
  <li>Try RooCode or <a href="https://cline.bot/">Cline</a> or ZooCode (RooCode’s new team) in <a href="https://code.visualstudio.com">VSCode</a> with your own local model, embedder and Qdrant. See what models work best for your language and style.</li>
  <li>Move onto <strong>Zed Editor</strong>. It’s an IDE centered on removing bloat and being fast, written in Rust with their own UI framework. Fewer problems than RooCode for me most of the time. <strong>remember_ex</strong> works with it.</li>
  <li><strong>Cursor IDE.</strong> I don’t trust their privacy. It’s VSCode with some extensions plus magic URL docs fetch when you thought you were fully using your local model, semantic search without ever asking you to configure an index or vector DB like RooCode does, something smells fishy, I feel they’re in the business of gathering repos. Your threat model may differ. I simply don’t trust them. I like their UI though, the way they structure their settings feels better thought that RooCode. They just hide too much stuff for me to trust them.</li>
  <li>Experiment with <strong>MCPs</strong>. I fork stuff at https://lmstudio.ai/maikelthedev and https://lmstudio.ai/tupik/top has the most incredibly useful README since lmstudio extensions have no directory for reasons that make no sense to me. The MCP standard is on https://github.com/modelcontextprotocol and you can get from there code that already works to tweak and make your own thing. My MCP started from their JS example “memory”, I rewrote it in Elixir. Preprocessor means LMStudio runs it <strong>before</strong> your prompt, hidden. MCP means the model decides to call it, visible. I wanted <strong>remember_ex</strong> in LMStudio and Zed so I gave up preprocessing perks for compatibility and ended up in that repo.</li>
  <li>Learn <strong>Qdrant</strong>. Simple, built for this. I haven’t tried every vector DB, they bore me. I just needed to understand enough to optimise the chunking. Qdrant does not get in my way.</li>
  <li>Do learn about <a href="https://pi.dev/">Pi.dev</a> since it is THE tool that lets you build your own harness easily and quickly.</li>
</ul>

<h3 id="3-learn-new-programming-languages">3. Learn new programming languages</h3>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1568716353609-12ddc5c67f04?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAzfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="Code on a screen, learning syntax and context with the harness doing the heavy lift" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/a-close-up-of-a-computer-screen-with-code-on-it-UMlT0bviaek" target="_blank" rel="noopener noreferrer">Code on a screen, learning syntax and context with the harness doing the heavy lift by Patrick Martin</a>
  </figcaption>
  
</figure>

<p>LLMs with internet and your own RAG for long-term memory and docs injection are ideal for learning new languages if you can ingest up-to-date docs. For Elixir the easiest path for me was ePub docs from projects, decompress, process, done. Efficient chunking is still the issue. Not a problem now but I keep thinking long term with massive libraries on Qdrant. Efficient chunking is about providing the LLM with JUST enough context to answer the question, that’s the key of the efficiency, the least information provided possible, the least context window you use. As context window grow the model becomes slow and hallucinates more, once you reach the limit you start afresh. Normally your harness compresses the context into a markdown file with just the important stuff and creates a new sesssion where it injects again. But there’s information loss during the compression so the longer you can remain before reaching the limit the better.</p>

<p>If the model remembers your style via long-term memory MCP or skills.md it can explain new programming languages in the exact way that you know worked for you before to learn the fastest. And we devs love flattening learning curves.</p>

<h2 id="specialisation-over-generic-omniscience">Specialisation over generic omniscience</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1727812100174-eeb12d758494?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTAzfA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="Notebook and keyboard, planning and integration over generic omniscience" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/an-open-notebook-sitting-on-top-of-a-desk-next-to-a-keyboard-gc3MKGke_9g" target="_blank" rel="noopener noreferrer">Notebook and keyboard, planning and integration over generic omniscience by Amanda Lawrence</a>
  </figcaption>
  
</figure>

<p>The next phase won’t be packing more trivia into one tensor. It’ll be integration. <strong>LSP</strong> (Language Server Protocol) level stuff already exists for deprecations, many LSPs already do boilerplate warnings, so it’s composition from multiple well-known packages added on top to create more complex boiler plate and reduce the boring and repetitive stuff. Company wikis plus small <strong>local</strong> models can help employees find information faster about processes, rules and how to do their job. Personal harnesses like <strong>pi.dev</strong> that feel like what <a href="https://neovim.io">Neovim</a> is for people who compose their editor, are tools that simplify creating your own harness tailored to you.</p>

<p>The tools to build this (RAG, MCP, Qdrant) are open-source, documented, affordable. The barrier is <strong>time</strong> and <strong>awareness</strong>, not permission from Scam Allmen. If you can write a Dockerfile you can deploy a local model. Guess what we’re good at, <strong>learning quickly</strong>, we have half the job done already.</p>

<h2 id="the-illusion-of-the-threat-to-job-security">The illusion of the threat to job security</h2>

<figure class="post-image post-image--unsplash">
  <img src="https://images.unsplash.com/photo-1758873269317-51888e824b28?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMTA0fA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" alt="A team around a table with laptops, collaboration and judgement over vendor hype" loading="lazy" decoding="async" />
  
  <figcaption class="post-image-attribution">
    <a class="post-image-source" href="https://unsplash.com/photos/diverse-team-collaborating-around-a-table-in-office-oiqFyLx_KDU" target="_blank" rel="noopener noreferrer">A team around a table with laptops, collaboration and judgement over vendor hype by Vitaly Gariev</a>
  </figcaption>
  
</figure>

<p>Answering my questions at the top, will LLMs affect our job security. I don’t think so. The threat is <strong>idiots</strong> who employ you might think AI will replace you and cut costs. It is not a surprise, all LLMs talk like overcaffeinated CEOs. We’re the users, they are the buyers. The buyer doesn’t need to understand the product, they see a vision to sell shareholders and believe it since it is talking in his same grandiose unbelievable to everyone else (but shareholders) tone.</p>

<p>You don’t need to be mindful of the LLM, you have to be mindful of how stupid your boss might be.</p>

<p>The future will be companies using LLMs as <strong>another frontend to data</strong>. LLMs will handle repetitive (boilerplate) parts of your job but everything that needs expertise (things that can go wrong), judgement (which stack, which CI, which OS, which database), creativity (front-end design is ridiculously basic and generic with LLMs), debugging, knowing package details, <strong>privacy</strong>, keeps being you.</p>

<p>The real change comes from engineers building custom tools. While those waiting for OpenAI or Anthropic to package the same solutions get left behind. And it won’t be because their LLM is smarter, it’ll be because the harnesses they build fit their work as a glove same as your own Neovim config makes you objectively faster.</p>

<p>Another funny ironic thing is how the more companies try to replace us, the more they will need us to fix all the fuck ups of unmanaged code written by the idiots who thought LLMs were reliable enough to be trusted.</p>

<p>I do think demand for prompt engineers is going away on its own, that stuff was always shallow next to real expertise on your programming language and frameworks. What it turns into is a smaller, more realistically sized niche of engineers who know how to design systems that work with natural language, from helpdesk chats that do not make customers hate your company to apps that answer from the company wiki when someone asks in plain language. For devs it’ll be just an improved boilerplate generator.</p>

<h3 id="direct-answers-to-the-three-questions">Direct answers to the three questions</h3>

<ol>
  <li>
    <p><strong>What can it do today?</strong> Fluent-ish text, images and audio and video, boilerplate, translation, brainstorming, and convincingly wrong answers unless you ground it with tools and sources.</p>
  </li>
  <li>
    <p><strong>How can it help or hurt my job?</strong> Helps on repetition, hurts when trust exceeds capability or when management believes the keynote.</p>
  </li>
  <li>
    <p><strong>Could it disrupt the economy or cost jobs?</strong> Money and attention will move. Mass unemployment of competent engineers isn’t what I’m seeing. <strong>The bubble that’s bursting is the idea that bigger closed models were the whole future.</strong> More like small models, clear tools, systems you own.</p>
  </li>
</ol>

<p>And again, none of this is A.G.I.</p>

<p>Have a good day!</p>

<p>For any questions find me in Mastodon as <a href="https://vmst.io/@maikel">@maikel@vmst.io</a> or in <a href="https://x.com/Maikeldotdev">Twitter</a> if you don’t mind waiting a hell of a lot longer since I’m a fediverse-first kind of person.</p>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="ai" /><category term="programming" /><category term="tools" /><summary type="html"><![CDATA[For software engineers: what LLMs actually are, why the hype may be cracking, and why harnesses and accuracy matter more than humongous SOTA models for job security.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://images.unsplash.com/photo-1736248991839-67d11f7a5643?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMzQ5fA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" /><media:content medium="image" url="https://images.unsplash.com/photo-1736248991839-67d11f7a5643?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzc3NjYzMzQ5fA&amp;ixlib=rb-4.1.0&amp;w=1200&amp;auto=format&amp;fit=crop" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hidradenitis Suppurativa: The Wegovy Way</title><link href="https://blog.maikel.dev/2026/03/13/wegoby.html" rel="alternate" type="text/html" title="Hidradenitis Suppurativa: The Wegovy Way" /><published>2026-03-13T00:00:00+00:00</published><updated>2026-03-13T00:00:00+00:00</updated><id>https://blog.maikel.dev/2026/03/13/wegoby</id><content type="html" xml:base="https://blog.maikel.dev/2026/03/13/wegoby.html"><![CDATA[<h1 id="why-im-taking-wegovy">Why I’m Taking Wegovy</h1>

<h2 id="living-with-hidradenitis-suppurativa">Living With Hidradenitis Suppurativa</h2>

<p>I have hidradenitis suppurativa, stage 2. Living with it means constantly thinking about things most people never have to worry about: what I eat, how much I sleep, what clothes I wear, how stressed I am, and whether any of those things might trigger a flare.</p>

<p>The disease itself isn’t just painful — it’s disruptive. When cysts appear in the armpits, sleeping becomes difficult. When they appear in the groin or buttocks, even sitting can become painful. After decades of dealing with this and going through several surgeries, avoiding those situations has become something close to an obsession.</p>

<p>Over time one factor has stood out more clearly than anything else: <strong>body fat percentage</strong>. The lower my body fat is, the less often I get flares.</p>

<p>One explanation involves <strong>TNF-α</strong>, an inflammatory protein strongly associated with hidradenitis. Fat tissue is one of the places where TNF-α activity tends to be higher. Since people with HS already have elevated inflammatory signalling, carrying more body fat seems to make flare-ups easier to trigger.</p>

<p>Because I’m stage 2, keeping inflammation under control matters a lot. Surgery is something I really want to avoid repeating.</p>

<h2 id="why-fat-loss-matters-so-much">Why Fat Loss Matters So Much</h2>

<p>For years the only reliable way I’ve found to manage this is by maintaining a relatively low body-fat percentage. The approach that works best for me is strength training. Building muscle makes it much harder for the body to accumulate fat, so keeping muscle mass high has always been part of the strategy.</p>

<p>I’ve been going to the gym consistently both before and after my last surgery. The goal wasn’t to become particularly big or strong — it was simply to keep fat levels low enough to reduce the chances of another flare.</p>

<p>But lately the strategy has started to run into a problem.</p>

<hr />

<h1 id="when-dieting-stops-working">When Dieting Stops Working</h1>

<p>I started doing cycles of <strong>bulking muscle and cutting fat</strong> more than six years ago. Back then, losing weight was relatively straightforward. I could drop around <strong>20 kg in about three months</strong> without much trouble.</p>

<p>Hunger was never the limiting factor. My usual method was simple: I would start a cut with a <strong>24-hour dry fast</strong>, which effectively reset my appetite and made the rest of the diet easy to maintain.</p>

<p>At 40, that approach doesn’t work anymore.</p>

<p>Part of the problem is medication. One of the drugs I take every day needs to be taken with roughly <strong>400 calories of food</strong>, and it has to be taken at about the same time each day. That alone breaks the fasting routine that used to work so well.</p>

<p>Another complication comes from ADHD medication. I alternate between <strong>Elvanse</strong> and <strong>Medikinet</strong>, and both of them interact poorly with many of the supplements I used to rely on during cutting phases. Stimulants like caffeine or synephrine interfere with them, while other supplements such as forskolin or L-carnitine tend to create unpleasant side effects or blunt their effect.</p>

<p>Because of that, aggressive dieting isn’t really an option anymore. The only realistic strategy left is a <strong>slow caloric deficit</strong>.</p>

<p>Unfortunately that creates another issue.</p>

<h2 id="the-inflammation-problem">The Inflammation Problem</h2>

<p>The longer I stay in a dieting phase, the more likely it becomes that inflammation ramps up and cysts start appearing again.</p>

<p>I’ve experimented with a lot of different approaches: alternating weeks of dieting and normal eating, slower cuts, different calorie cycles. None of them solved the problem consistently. Eventually a cyst would appear and I’d have to abandon the cut entirely and go back to maintenance or bulking.</p>

<p>Over time the result of this cycle has been predictable.</p>

<p>I now weigh <strong>97 kg</strong>, and while a lot of that is muscle, it’s also more mass than I ever intended to carry. Being heavier is starting to affect my back, and if I don’t interrupt this cycle it will only get worse.</p>

<hr />

<h1 id="why-i-decided-to-try-wegovy">Why I Decided to Try Wegovy</h1>

<p>For a long time I’ve been following the research around <strong>semaglutide</strong> and other GLP-1 medications. Wegovy is essentially the weight-loss version of semaglutide — the same active ingredient used in Ozempic, but dosed specifically for obesity treatment.</p>

<p>Most people think of these drugs purely as appetite suppressants, but they actually do several things at once. They reduce appetite, slow stomach emptying, regulate blood sugar, and may also have anti-inflammatory effects.</p>

<p>That last point is what caught my attention.</p>

<p>If semaglutide really does reduce inflammatory signalling, it might help keep TNF-α activity lower while I’m dieting. In theory that could allow me to stay in a caloric deficit long enough to lose fat without triggering another hidradenitis flare.</p>

<h2 id="starting-dose">Starting Dose</h2>

<p>Last night I took <strong>0.25 mg</strong>, which is the lowest available dose. This is the standard introductory dose used to let the body adjust before increasing it.</p>

<p>For now I’m deliberately staying low. I’m not actually looking for extreme appetite suppression. Hunger was never my biggest obstacle. What I’m really interested in is whether the medication can make dieting <strong>less inflammatory</strong>.</p>

<p>If the lowest dose already helps, I might never need to increase it.</p>

<hr />

<h1 id="day-one-what-it-felt-like">Day One: What It Felt Like</h1>

<h2 id="the-morning">The Morning</h2>

<p>The first thing I noticed when I woke up was that I felt <strong>less hungry than usual</strong>. That’s one of the most commonly reported effects of GLP-1 drugs, so it wasn’t surprising.</p>

<p>My energy at the gym, however, was strange.</p>

<p>I had no problem getting up, preparing for the gym, and starting my workout. But halfway through my deadlift session I suddenly felt completely drained. My routine always begins with the two most important exercises in case I have to cut the session short, and that’s exactly what happened.</p>

<p>It was back day, and I managed to complete six total sets across two exercises. After that I was done.</p>

<p>I sat on a <strong>recumbent bike at the lowest resistance for about twenty minutes</strong> while my partner finished training. Even walking to the showers felt unusually heavy.</p>

<p>Another odd detail was body temperature. I had to wear my <strong>thick winter jacket</strong> on the way to the gym even though I’m usually someone who runs hot. My hands felt freezing the entire time.</p>

<p>That said, I’m not sure I can blame semaglutide for that. Something similar has happened before when <strong>Elvanse peaks during heavy workouts</strong>, especially on deadlift or leg days. Today I took it earlier than usual because GLP-1 drugs slow digestion, and the timing might simply have been off.</p>

<p>So that part remains a question mark. 2</p>

<h2 id="midday">Midday</h2>

<p>After leaving the gym and have some sugary drink to counteract the low-blood sugar effect, I felt fine again and I remembered I had a <strong>lunch meeting with my former boss</strong> on the other side of the city. Getting there required walking uphill at a pretty fast pace, which surprisingly felt completely fine.</p>

<p>The interesting part happened when I sat down to eat.</p>

<p>Normally after a workout I’m ravenous. Instead I ordered <strong>two chicken fillets and a Mediterranean salad</strong>, and that was more than enough. The feeling was very similar to what happens after my usual <strong>24-hour fasting reset</strong>, except this time I hadn’t fasted.</p>

<p>Something else felt different too.</p>

<p>People often describe GLP-1 medications as eliminating “food noise”. In my case it feels more like <strong>all mental noise is reduced</strong>. My brain simply feels calmer and more controlled.</p>

<p>Some people with ADHD report something similar on these drugs, and that description matches what I’m experiencing surprisingly well.</p>

<h2 id="digestion">Digestion</h2>

<p>So far there have been <strong>no digestive issues</strong>. If anything, my stomach actually feels better than usual. By this time of day I’m normally a bit bloated, but today that hasn’t happened.</p>

<hr />

<h1 id="what-i-actually-ate-today">What I Actually Ate Today</h1>

<p>Despite going to the gym, my total intake so far has been fairly small. I’ve had a protein shake with creatine, two chicken fillets, a Mediterranean salad with olive oil and vinegar, and a few drinks throughout the day: an orange Fanta, a sugar-free Aquarius, a glass of Albariño wine, and a small espresso.</p>

<p>The espresso was necessary — my digestion is already slow even without semaglutide, and I suspected those chicken fillets might otherwise stay in my stomach until next year.</p>

<p>Interestingly, the <strong>salad tasted fantastic</strong>, while the heavier dishes on the menu looked strangely unappealing. That’s very unusual for me after training. Normally after deadlifting <strong>80 kg for three sets of ten repetitions</strong>, I feel like I could eat an entire cow, farmer included.</p>

<hr />

<h1 id="mood-and-energy">Mood and Energy</h1>

<p>Mood-wise I feel calm and focused, without any irritability.</p>

<p>Energy levels are harder to judge because the early gym crash complicates things. But after drinking the Fanta I recovered quickly, and walking uphill across the city didn’t feel difficult at all. Now, late in the afternoon, I also haven’t experienced the usual crash that tends to happen after lunch.</p>

<hr />

<h1 id="interaction-with-adhd-medication">Interaction With ADHD Medication</h1>

<p>Today wasn’t the ideal test because <strong>Elvanse peaked earlier than expected</strong>.</p>

<p>Normally I alternate between <strong>Elvanse (50 mg)</strong> and <strong>Medikinet (60 mg)</strong>. For reasons I still don’t fully understand, both medications seem to work better <strong>for me</strong> when they’re alternated instead of taken continuously. It is as if I develop tolerance if I continue with the same but not when I alternate.</p>

<p>Tomorrow I’ll repeat the experiment with Medikinet and see whether the experience feels different. Back on Elvanse the day after I will take it later.</p>

<p>Realistically it will probably take several days before I can separate normal fluctuations from actual effects of the medication.</p>

<hr />

<h1 id="early-thoughts">Early Thoughts</h1>

<p>It’s obviously far too early to draw strong conclusions, but the first day with Wegovy has been interesting.</p>

<p>The most noticeable differences so far are the near absence of cravings, a much smaller appetite after training, and a general sense of mental quietness that I didn’t expect and are making me considering even trialing a few days ADHD meds free.</p>

<p>If those effects remain consistent over time, semaglutide might finally make it possible to lose fat <strong>without triggering hidradenitis flare-ups</strong>. And if that turns out to be true, it could change the way I manage this disease entirely. Reducing the time left to achieve 11% body fat and forget about it for good to merely months.</p>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="hidradenitis" /><summary type="html"><![CDATA[My first experience with Wegovy as a hidradenitis suppurativa sufferer and how it's affecting my body today.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/wegovy.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/wegovy.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">I Tried Cursor and I’m Not Worried</title><link href="https://blog.maikel.dev/2026/01/02/i-tried-cursor-and-im-not-worried.html" rel="alternate" type="text/html" title="I Tried Cursor and I’m Not Worried" /><published>2026-01-02T00:00:00+00:00</published><updated>2026-01-02T00:00:00+00:00</updated><id>https://blog.maikel.dev/2026/01/02/i-tried-cursor-and-im-not-worried</id><content type="html" xml:base="https://blog.maikel.dev/2026/01/02/i-tried-cursor-and-im-not-worried.html"><![CDATA[<p>The image I’ve chosen from Unsplash for this article is no accident at all, keep reading and you’ll figure out why.</p>

<p>I recently survived showing someone <code class="language-plaintext highlighter-rouge">sparkr_private</code>, a repo containing a prototype version of <a href="https://codeberg.org/maikelthedev/sparkr">Sparkr</a> I built in React Native within a week. The goal wasn’t to build production code. It was to rapidly test <strong>feasibility</strong> of ideas, disposing of concepts quickly just to see what could actually be possible based on ideas in my head.</p>

<p>The result? Shameful code. Heavily written with the help of LLMs (specifically Cursor), it’s 100% slop built on top of slop, designed to answer one question after another:</p>

<ol>
  <li>Is this possible? Yes.</li>
  <li>Ok, is this possible? Yes, ok, next.</li>
  <li>Is this possible? Yes, ok, next.</li>
  <li>Is this possible? No? Ohhh, what if I do it this way? Yes, ok next.</li>
</ol>

<p>Rinse and repeat until I had all my questions answered.</p>

<p>The code <strong>individually</strong> worked, but integrated would probably make any mobile phone explode. There were no test suites, just monkey trialing after each idea. No integration tests. Once a feature was proved feasible I run the app on my phone, tested it manually, then I read the code to understand how, then I moved onto checking how the next one could be done. I guarantee you most new ideas broke the previous ones. I’m still debating with myself whether to make that code public.</p>

<p>But here’s the thing: it worked for its purpose. I learned that NIP-17 e2ee direct chat was possible (even if implemented in extremely wasteful and verbose ways) to add to a chatting app easily. I proved uploading facepics using free-to-use <a href="https://github.com/nostr-protocol/nips/blob/master/B7.md">Blossom relays </a>could work. I validated a grid <strong>directory</strong> system based on relays passed via app signature, posted in public relays.</p>

<p>From there onwards, I had all I needed to know it was feasible.</p>

<h2 id="the-only-valid-use-case">The Only Valid Use Case</h2>

<p>After this experiment, I’ve found the only valid use case for LLMs in software development: testing feasibility of random ideas on disposable code to motivate yourself enough to code them yourself.</p>

<p><strong>Once you’ve seen it, you want to make it real</strong>. You’re no longer working with an idea in your head. You answered each and every question you could have about it, to completion, and saw its full potential. It’s a bit like experiencing the joy of seeing your own children grow up and get married, knowing you made it happen, you raised a good kid, before actually deciding to have children. There’s no what if, there’s only a clear path. There’s no scrum or agile ever changing requirements, you’ve gone from specification to final broken thing that kind of works. There’s no ifs, there’s no uncertainty. You’ve seen it, had it in your hands, made of your ideas. Ignore the code, you just wanted to know if it could exist. You know now it can exist. It is <strong>a certainty</strong>.</p>

<p>Or, put another way: helping you gather answers to all the burning questions you might have on a software project before you write the first line of actual decent code.</p>

<p>This is genuinely useful. When you’re exploring a new domain, trying to understand if something is technically possible, or rapidly prototyping to validate concepts, discard what you figure doesn’t work and move onto quicker into alternative options.</p>

<p>But that’s where it ends.</p>

<h2 id="where-llms-fail-catastrophically">Where LLMs Fail Catastrophically</h2>

<p>If you’re thinking about using LLMs for actual software development, here’s where they IMHO fall apart:</p>

<h3 id="1-elegance">1. Elegance</h3>

<p>LLMs don’t understand elegance. They don’t grasp clean architecture, beautiful abstractions, or thoughtful design patterns. They’ll give you code that works, but it’s the software equivalent of a functional but ugly building. It stands, but you wouldn’t want to live in it. Stupid ridiculous thing never had to pass an exam on algorithms, data structure or Big O notation, <a href="https://www.open.ac.uk/courses/modules/m269/"><strong>you did.</strong></a></p>

<p>They’ll write stuff like this 👇</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">doStuff</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">,</span> <span class="nx">c</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">a</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">a</span> <span class="o">!==</span> <span class="kc">undefined</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">a</span> <span class="o">!==</span> <span class="dl">""</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">b</span> <span class="o">==</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">if </span><span class="p">(</span><span class="nx">c</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">doing stuff</span><span class="dl">"</span><span class="p">);</span>
            <span class="k">for </span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">c</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
              <span class="nf">setTimeout</span><span class="p">(</span><span class="nf">function </span><span class="p">()</span> <span class="p">{</span>
                <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
              <span class="p">},</span> <span class="mi">1000</span><span class="p">);</span>
            <span class="p">}</span>
          <span class="p">}</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>
<p>There are some basic principles of software development that they simply ignore:</p>

<ul>
  <li>KISS, they write incredibly long lines of code with no specialisation or compartimentalisation in smaller functions, it’s untestable due to how long and how many variables it depends on .</li>
  <li>DRY, they write the same lines across entire codebases again and again.</li>
  <li>….you can find the whole damn list here in Wikipedia. No one who’s studied software engineering would be unaware of the existence of those and many other rules.</li>
</ul>

<p>Anyone who’s coded for a couple of years can easily identify code written by a LLM compared to code written by a human.</p>

<h3 id="2-refactoring-and-global-view">2. Refactoring and Global View</h3>

<p>Ask an LLM to refactor code, and you’ll get a mess. They don’t understand the subtle relationships between components, the reasons behind certain design decisions, or how to improve code while maintaining its integrity. They’ll <strong>change things that shouldn’t be changed</strong> and <strong>leave problems that should be fixed.</strong></p>

<p>They’re terrible at handling project specific requirements, domain knowledge, or nuanced business logic. They’ll give you generic solutions that don’t quite fit your actual needs. The devil is in the details, and LLMs are detail blind because they can’t see things <strong>globally</strong>. Why? Context. Remember, they aren’t human.</p>

<p>I’ll talk more about this context thing later, but this is what makes them behave like elderly people with 👇 and the main reason I’ll never fear them.
<img src="https://images.unsplash.com/photo-1619963030941-69eb5b7ef496?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDEzfHxhbHpoZWltZXJ8ZW58MHx8fHwxNzY3MzkxNTMzfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="white and yellow letter t" /></p>
<h3 id="3-testing">3. Testing</h3>

<p>Tests written by LLMs are <strong>awful</strong>. They test the wrong things, is not that they miss edge cases, but that they implementation details rather than behavior. Good testing requires understanding the system, its requirements, and potential failure modes, all things LLMs struggle with because of <strong>a lack of a global view.</strong> Context, my friend, that’s why you’ll always have a job.</p>

<h3 id="4-anything-involving-refinement">4. Anything Involving Refinement</h3>

<p>LLMs are one-shot generators. They don’t iterate well. They don’t learn from feedback in a meaningful way. Real software development is iterative refinement, taking something that works and making it better, cleaner, more maintainable. LLMs can’t do this. You can tell them to rewrite the same function ten times and I gurantee you by the fourth time you’ll get again the first version. Again, why? Repeat after me: <strong>context.</strong></p>

<h3 id="5-language-support-beyond-python-and-javascript">5. Language Support Beyond Python and JavaScript</h3>

<p>LLMs fail catastrophically at any language that’s not Python or JavaScript. In Elixir, for example, they even make up non-existent functions of core modules. They’ll confidently suggest core-language functions that don’t exist, use incorrect syntax, and provide solutions that simply won’t compile or run. Let alone they are <strong>dated by design</strong>. Why do you think their best language for any of them to prototype with is JavaScript?</p>

<h2 id="the-junior-dev-fallacy">The “Junior Dev” Fallacy</h2>

<p>People who defend LLMs as if they were the 2nd cumming (typo intended) of Jesuschrist often say:</p>

<blockquote>
  <p>It’s like sitting with a junior developer.</p>
</blockquote>

<center><strong>No, it is not.</strong></center>

<p>A junior developer <strong>thinks</strong>. They ask questions. They learn. They make mistakes and understand why they made them. They <strong>grow their knowledge</strong>, they refine, they get better. They bring human judgment, <strong>creativity</strong>, and reasoning to the table.</p>

<p>An LLM vomits. It generates text based on patterns it’s seen, without understanding, without reasoning, without the ability to truly learn from <strong>context</strong> in the way a human does.</p>

<p>But here’s the critical difference that makes human coders irreplaceable:
<strong>context</strong>.</p>

<p><img src="https://images.unsplash.com/photo-1762894764362-d264b9ffc92a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDE0M3x8Y29udGV4dHxlbnwwfHx8fDE3NjczOTIzOTl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="People waiting at a mural-adorned bus stop" /></p>
<h1 id="what-do-you-even-mean-with-context-maikel">What do you even mean with “context” Maikel?</h1>

<p>A <strong>human</strong> developer can maintain a massively <strong>humongous</strong> context. They can hold in their head the entire architecture of a system, understand how components interact, remember why certain decisions were made months <strong>if not years</strong> ago, see the big picture and the small details simultaneously. They can trace through code mentally, understand the flow of data, see relationships that aren’t explicitly documented, and make connections across the entire codebase. They have <strong>a global vision.</strong></p>

<p>I have ADHD yet unlike most, when I’m unmedicated, my working memory is so large I can hold in my head the entire architecture of an idea for weeks if not months. I can think through complex systems, see how everything connects, and maintain that mental model continuously while I try pseudocode in my head.<strong>An LLM can’t do more than a few prompts without forgetting the entire subject.</strong> Even the most advanced models with massive context windows can’t maintain that kind of persistent, deep understanding. They reset. They forget. They can’t hold onto the architecture the way a human mind can.</p>

<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">
    <p><strong>Context</strong>: There’s a very tiny window of information you can supply to them, past this window they start from scratch as if an elderly person with dementia.</p>
  </div></div>

<p>That’s what <strong>context</strong> means for an LLM. he bigger the context size, the slower they get and they <strong>all</strong> have this limitation. They’ll never get global-type of understanding to any codebase unless it is so tiny it fits entirely in their context, together with the much larger amount of reasoning of what it does, why it does it, etc.</p>

<p>Any LLM, even with the largest context windows available, fails precisely where humans excel. Context windows are limited. They can’t truly “remember” beyond what’s in the current conversation. They can’t maintain the kind of deep, interconnected understanding that a human developer builds over time working with a codebase. They process text, not meaning. They see tokens, not systems.</p>

<p>This is where human coders will always prevail. Real software development isn’t about writing isolated functions. It’s about understanding complex systems, maintaining mental models of how everything fits together, and making decisions that consider the entire context of the project, its history, its future, and its constraints. LLMs can’t do this. They can only work with what’s explicitly in their context window, and even then, they don’t truly understand it.</p>

<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">
    <p><strong>Moral of the story</strong></p>

    <p>You cannot write professional code with an LLM. 
You can use it to test the feasibility of some ideas and that’s it.</p>
  </div></div>

<h2 id="the-professionals-approach-that-imho-should-be-the-gold-standard-on-llm-usage">The Professional’s Approach that IMHO should be the gold-standard on LLM usage</h2>

<p>So how do you use LLMs without destroying your career or your codebase?</p>

<p>First, <strong>assume their knowledge is based on inaccuracies</strong>. Don’t ask them if something is feasible. Direct them to code your idea, so you can see with your own eyes if it is and fix what it gets wrong.<strong>Never use LLMs as juries of what is possible</strong>. Never take their opinion. They aren’t people, <strong>they are all snake oil salesmen.</strong> Their job is to sound knowledgeable, not to produce actual knowledge. Do you think the guy at MediaMarkt has any clue of the actual power of an i7-13123 vs an AMD Ryzen 7 Gen 8 v2.5 (I just made up tha last part)? His job is neither to draw sillicon logic in ever shrinking transistor size, with an ever increasing set of instructions to process things ever quicker, using ever more complicated manufacturing processses. Nope, that’s not his job, his job is to sell.</p>

<p>So instead focus on making them do quickly the prototypes of ideas you had in your head. Think of it as a feedback loop overcharged of the ideas that you will ask yourself over a number of weeks, in just days or 24 hours. But ultimately the machine would answer them a lot quicker, just a lot less accurately and more constrained. The prototype you end up with is just one of hundreds of millions of possibilities. <strong>The box cannot think outside the box.</strong> YOU CAN.</p>

<h3 id="1-use-a-different-language-for-prototyping-one-you-dont-care-much-about">1. Use a Different Language for Prototyping one you don’t care much about</h3>

<p>Never use an LLM for the final programming language you’re going to use. Only use it for with shite error-ignoring-prone languages that you don’t get paid professionally to use anymore, like JavaScript (if that’s not your main language).</p>

<p>Why? This ensures your knowledge of your food-on-the-table language remains intact due to constant use <strong>unaided</strong>. It also avoids future <strong>licensing</strong> issues. Most importantly, it makes copy and paste between prototype and final product impossible.</p>

<p>So prototype ideas on a shite language, write them yourself on the proper one.
<img src="https://images.unsplash.com/photo-1692734207733-a79de344ff3a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDQ2fHxzaGl0fGVufDB8fHx8MTc2NzM5MzU5M3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="a red sign with a picture of a cow on it" /></p>
<h3 id="2-never-use-it-with-your-professional-language-or-the-one-you-feel-passionate-about">2. Never Use It With Your Professional Language or the one you feel passionate about</h3>

<p>If JavaScript is your professional language, use Python for prototyping. The separation is crucial.</p>

<p>Here’s my personal approach: JavaScript was my professional language for quite a while. I can still read it and code in React, Angular, React Native, or NativeScript when designing quick prototypes for Android, Angular was paying the bills. But I haven’t used it professionally in a very long time, nor do I think I will. My main <strong>decent</strong> languages that I get paid sustenance money for are Elixir, which I deeply love, and hopefully soon Kotlin, which finally covers the problems of Java’s obsessive OOP, and finally able to do what React Native had for me: multiplatform, thanks to Kotlin Multiplatform.</p>

<p><strong>I need to know what I’m doing with these languages,</strong> so no LLMs with them. Even better if you can use pseudocode for prototyping. Pseudocode lets you think through the logic and architecture without committing to any specific language, and then you can implement it properly in your professional language yourself. This maintains your skills while still allowing you to rapidly prototype ideas.</p>

<p>We need to compartmentalize: code we write from code LLMs produce. The best way is to use it to understand a concept, read through it, close the fucking tool, and force yourself to write from scratch the code yourself in that different language. As I said before: test the feasibility of ideas, never their actual implementation.</p>

<p>The main reason is the one I mentioned earlier, licensing issues. Imagine you work on a project, you worked aided by some LLM and then months down the line you discover your code comes (inevitably) from copyrighted code with an incompatible viral license to what you’ve “written” for someone else. I’m not talking about some for loop, I’m talking about those <strong>naive</strong> fake-fluencers trying to sell you books, seminars, and other bullshit, about completely code-free coding using agentic-mode only. There’s no way they aren’t ending up with large sections of “their” code being copyright-breaking material with lawyers in-waiting. This is going to probably end up being so big, these kinds of ads will have to be created with different banner “<strong>has your code being used in a large codebase elsewhere, no win, no fee</strong>”.
<img src="/assets/images/2026-01-image-1.png" alt="Image" /></p>
<h3 id="3-use-local-models-when-possible">3. Use Local Models When Possible</h3>

<p>Swap for local models with tools like Ollama as soon as you can. But be VERY aware: they are a black box. Every line of code out of an LLM is code stolen from somewhere and that somewhere clearly has a license just as much as anything sucked by a commercial-provider. NEVER copy and paste.</p>

<p>Even with local models, you’re dealing with code that was trained on, <strong>always assume, copyrighted material</strong>. The licensing implications are enough to remember you cannot and should not use their code.</p>

<p>The local models have a big advantage: nothing leaves your PC. The code you write, using a commercial LLM, always end up on someone else’s server. Cursor has a setting to keep thinks locally, I assume that setting is misleading.<strong>Never put the code you write for one of your clients on LLMs but specially online ones.</strong> Assume the company behind it is harvesting your prompts for further refinement of the model, and that everything you enter on it, will be used by someone else later. You might be digging your grave.</p>

<h3 id="4-never-invest-in-this-tech">4. Never Invest in This Tech</h3>

<p>Don’t buy GPUs to use them locally, don’t get the greatest 64 GB Apple Sillicon laptop to try different models locally, unless you have the spare funds. Assume it’ll vanish into thin air. Don’t put your value on requiring them. Never develop dependency on them.</p>

<p>Nothing depreciates faster than software, and this is clearly an unsustainable bubble on snakeoil sales-speech software.</p>

<p>You’ve lived through Uber, Airbnb, and other venture capitalistic cunts. You know what OpenAI and others will do. This shit is cheap now, <strong>it’ll cost 10 times more in the near future</strong>. They are burning through VC cash to get you hooked into it. Same as Uber did while destroying their local taxi competitors, and now they are pricier than local taxis. Same as Starbucks does opening multiple stores to asphyxiate the competitors. It’s all the same strategy: cheap now, gets you hooked, become a commodity, then raise the prices.</p>

<p>Don’t build your career or your projects on something that will become unaffordable once they’ve eliminated the competition and you’re locked in.</p>

<p>The LocalLLM market is entirely different, and for certain do investigate on it for fun as much as you’d like as I think this sub-are of LLMs is what will eventually rise victorious.</p>

<h3 id="5-maintain-self-control">5. Maintain Self-Control</h3>

<p>All of this requires self control. How much of an adult are you? How much do you value your employability?</p>

<p>The temptation to use LLMs for “real” work is strong. Short term might feel like you can do one week’s worth of work in an couple of hours. But in the long term, it erodes your skills, creates technical debt, produces code that’s harder to maintain, and makes you dependent on a service that will become eventually either unaffordable to you or unusable due to licensing issues.</p>

<p>Think about this: if running a local LLM that produces anything near the quality of Cursor’s default model costs you a humongous debt in computer components, what incentive do the already existing commercial ones have to NOT eventually raise what they charge for it. They aren’t NGOs, they are all for-profit companies.</p>

<p>It’s a fad, it’ll go and evolve into mini-local LLM models.</p>

<h2 id="conclussion-why-the-rubbish-image-and-why-our-future-is-safe">Conclussion, why the rubbish image and why our Future is Safe</h2>

<p>I don’t think professional software development is going anywhere in favour of machines doing it.</p>

<p>The people who will ignore this advice? Clearly those who don’t work professionally as coders. Professional developers understand that code isn’t just about making something work, it’s about making something work well, maintainably, and elegantly. It’s like raising a kid.</p>

<p>But I don’t think we’re through the worse yet. I think first, will come the commodifying of LLMs, getting as many people hooked as possible, second the hiking of the prices, third, the selling of the convenience of commercial ones compared to local LLMs, then legislation will FINALLY catch up (if ever) and burst the bubble of the commercial ones (that’s where the injury-lawyer industry re-focus will happen into <strong>some major copyright battles</strong>), then some local LLMs will become easier to understand and run, then there’ll be probably more openness (enforced by the law, probably the EU first) about the source of LLMs “knowledge” and finally purpose-built per coding-language and ecosystem language models. Tiny thematicals LLMs capable of requiring less power since they are focused on a single thematic task (like, future Jetpack Compose UI generators).</p>

<p>I’m expecting the bubbele to burst spectacularly and in a similar way the housing crash did since we’re humans, which means, our context window is neither infinite and love repeating our own mistakes. Every bank surely has directly or indirectly invested in this crap, and the moment OpenAI crash we’ll have another Lehman Brothers moment</p>

<blockquote>
  <p><strong>OpenAI is the new Lehman Brothers</strong></p>
</blockquote>

<p>Now, why that image? Because when I think of Sillicon Valley and Big Tech™️, that’s what comes to my mind: rubbish and people going through it, trying to find value. Yet instead of looking in the recyclable bin, where by definition it is easier to find something that worked in the past and refine it, they look into the non-recyclable one, the one filled with dog poo and cat litter, and try to sell it to us as The Cum of Christ, Microsoft is particularly guilty of this, no matter how many times people shout at them “I don’t want to eat a used diaper” they persistently repeat “now with Premium Poo”👇</p>

<center><strong>No, thank you.</strong></center>

<p>LLMs are a tool. Like any tool, they’re useful for specific tasks and terrible for others. Use them for what they’re good at: rapid feasibility testing on disposable code. Don’t use them for what they’re bad at: actual software development.</p>

<p>Our future is safe because professional software development requires skills that LLMs simply won’t ever have due to their main limitation: context.</p>

<p>The developers who will thrive are the ones who understand this distinction and maintain their skills accordingly. The ones that will have to re-skill themselves into some other industry are those who fell for the vibe-coding fad.</p>

<p>Thanks for coming to my house, tip your waitress. Byyeeeee!</p>
<figure><iframe src="https://tenor.com/embed/5266957" frameborder="0"></iframe></figure>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="programming" /><category term="tools" /><category term="AI" /><summary type="html"><![CDATA[I tested Cursor, for prototyping ideas to just figure if they are feasible it works, yet for production level it doesn't. Context is their kryptonite. Our jobs are VERY MUCH safe.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1571441249554.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1571441249554.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Living Out Loud With ADHD – Episode 3: Elvanse long-term</title><link href="https://blog.maikel.dev/2026/01/02/living-out-loud-with-adhd-episode-3-elvanse-long-term.html" rel="alternate" type="text/html" title="Living Out Loud With ADHD – Episode 3: Elvanse long-term" /><published>2026-01-02T00:00:00+00:00</published><updated>2026-01-02T00:00:00+00:00</updated><id>https://blog.maikel.dev/2026/01/02/living-out-loud-with-adhd-episode-3-elvanse-long-term</id><content type="html" xml:base="https://blog.maikel.dev/2026/01/02/living-out-loud-with-adhd-episode-3-elvanse-long-term.html"><![CDATA[<h1 id="some-necesary-background">Some necesary background</h1>

<p>Quick mandatory reminder 👇</p>
<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">⚠️</div><div class="kg-callout-text">
    <p><strong>This is not medical information</strong>, nor am I a trained physician or any kind of expert in ADHD. I’m just another ADHDer untangling the data I got and detailing my own personal experience and effects (if any) of meds.</p>

    <p>If you think any of this relates to you, don’t take it on your own hands, discuss it with your doctor. If any of it particularly helped you or you think I might be on the wrong track feel free to let me know. Interactions are <strong>encouraged</strong>.</p>
  </div></div>

<h1 id="some-issues-ive-encountered-from-trying-to-operate-in-a-way-i-am-not-used-to">Some issues I’ve encountered from trying to operate in a way I am not used to</h1>

<p>I think the main issue we’ve got to discuss globally is the fact I’ve functioned 39 years more or less succesffully without any medication. That means I have 39 years of workarounds and systems put in place, and behaviours to control what I thought was generic distractability and procrastination. For the most part, my ADHD only bothers me half the day when I’m not medicated, when these ways have exhausted my brain as even as hardcoded as they are, they are still forcing a machine to work in ways it shouldn’t be doing.</p>

<p><strong>The main good thing:</strong> On meds I start lots of things that I normally don’t even have the drive to start, and I would do them to completion, but in the path to that; <strong>the main bad thing:</strong> everything else gets accumulated.</p>

<h2 id="1-priorities-are-hard-to-measure-with-extra-dopamine">1. Priorities are hard to measure with extra dopamine</h2>

<p>Without any extra dopamine all tasks feel as if they have the same weight so when you try to trust your guts and do what “feels right” you’re indeed doing what reason tells you it is right.</p>

<p>With dopamine in abundance, that doesn’t work, what feel rights is actually a scale of different things, the ones you do enjoy have a much bigger weight and the ones that were reasonably important become somehow less so. It’s ridiculously easy to lose track on something you were even slightly passionate about and realise when it is too late in the day.</p>

<p>Deciding on priorities is a lot harder off the meds. I cannot stop a task until I finish it with meds, the problem is that task might not be the most important one for that day.</p>

<h2 id="2-the-sargent️-is-missing-and-lifo-does-not-work">2. The Sargent™️ is missing and LIFO does not work.</h2>

<p>Unmedicated I normally have this inner voice I call “The Sargent” that reminds me what’s the main task I’m doing every few minutes and what other things I have to do, so I don’t lose track of what I’m doing with some distraction. That voice is gone now. So what keeps me on track is now passion, which is easy to let myself get carried away with.</p>

<p>So my days require a lot more planning because whatever I’m doing I’m going to lose track of any other thing I had to do that day unless I keep checking.</p>

<p>Normally, unmedicated LIFO (last-in, first-out) works, I can be doing a main task and distractions come in that are necessary to remove to carry on with main task. This stack scheduling does not work at all medicated. The stack just keeps getting bigger because I keep forgetting the main task and getting carried away. So the only thing that works for me is to outsource The Sargent.</p>

<p>I have a humongous Fish shell script that prints a banner with the main task, duration, when I stated and whenever I get distracted, I run another command from that script file to record the distraction. The whole system is markdown based and kept in my obsidian vault, except the Fish shell file.
<img src="/assets/images/2026-01-image-2.png" alt="Image" />
So today for that we get this, so far:
<img src="/assets/images/2026-01-image-3.png" alt="Image" />
Without this artificial version of The Sargent, I wouldn’t get anything done at all. So I’m introducing automation left and right to avoid making decisions the whole day and wasting hours on analysis paralysis.</p>

<p>I’m also going to rewrite this tool to remind me every 5 minutes of the task I’m doing instead, I just need to figure how to disable all other desktop notifications in the process.</p>
<details>
  <summary>Update on 7th of January of 2026</summary>

  <p>I made my script show this in the middle of the screen using Zenity every five minutes. I think as it is it could be useful to other people so <a href="https://gist.github.com/maikelthedev/9695bcec08f85e36a613c7bcbdc3cd09#file-add-distraction-fish-L691">here it is.</a></p>

  <h3 id="how-it-works">How it works</h3>

  <ol>
    <li>
      <p>My daily journals in Obsidian have a section called “What I do want from today” in there every day I write the main tasks I need to get done.</p>
    </li>
    <li>
      <p>In hyprland if I click Win+I it inserts a distraction, by asking me what is distracting me using Zenity for GUI creation from Fish shell.</p>
    </li>
    <li>
      <p>If none of the tasks are currently active, it’ll ask me which one is the main one I’ve been distracted from, once clicked, it’ll mark when I started it.</p>
    </li>
    <li>
      <p>Every five minutes that box below will show on the screen, you can clearly change this, but 5 minutes is enough for me, i can get rid of it with a quick enter key press, no mouse needed. YES, I WANT IT AS INTRUSIVE as I normally am off meds, that’s how bad it gets on meds for me. I want it to mimic the behaviour of my internal scheduler.</p>
    </li>
    <li>
      <p>It creates urgency on the main task.</p>
    </li>
    <li>
      <p>I can move onto a new task by the shortcut Win+Ctrl+I, that runs “new-task” and writes on Obsidian the time it took to complete the previous one. If you have the right skin in Obsidian it’ll change “[/]” for “[x]” and you’ll notice it looks differnet, not all themes of Obsidian show “[/]” different to “[x]”. The “[/]” exists to mark when a tasks is ongoing (not completed, but started). It’ll also write there when I finished and how long it took.</p>
    </li>
  </ol>

  <p>It is incomplete but so far useful. Of course you can make the keys be whatever you want or use none and just call it from the terminal. The keys are not defined anywhere, they are just key bindings in my hyprland config to commands of the script.</p>

</details>

<p><img src="/assets/images/2026-01-image-7.png" alt="Image" /></p>
<h2 id="4-tyrosine-matters">4. Tyrosine matters</h2>

<p>This is plain simple, less than 1 gram of tyrosine supplementation per day and the effects of Elvanse (Medikinet too) were jerky or erratic. Get the gram of tyrosine (at least first thing together with the Elvanse) and the effect just maintains the whole day.</p>

<p>So now I’m force to supplement myself with tyrosine. At least is cheap.</p>

<h2 id="5-sleeping-is-better-but-more-rigid-in-some-ways-and-flexible-in-others">5. Sleeping is better but more rigid in some ways and flexible in others</h2>

<p>I’m slowly realising I can sleep anytime, at any moment when I want to do so, but under some rules. Being able to put the mind in blank is the main rule, which I can do under the effect of the meds. Once the effect starts to fade it becomes increasingly harder.</p>

<p>It doesn’t matter much when I wake up, what I’m realising matters <strong>the most</strong> is that I stay consistent in the time I take the pill. I can take it at 7am every day and still go back to sleep. If I don’t, and I take it whenever I wake up and that time is noon, I’m going to struggle to go to sleep early that day, and then we enter a vicious circle of taking the pill each day later and going to sleep later. So taking the medication at the exact same time every day is crucial, more so than waking up every day at the same hour.</p>

<p>So now I have the pill prepared with a lot of water by the bed with the tyrosine.</p>

<h2 id="6-discovering-im-a-workaholic-and-how-this-is-negative">6. Discovering I’m a workaholic and how this is negative</h2>

<p>I’ve got lots of drive but in too many directions which generates procrastination by analysis paralysis. I want to:</p>

<ul>
  <li>go to the gym</li>
  <li>lose fat (not weight)</li>
  <li>have a sleep schedule</li>
  <li>work</li>
  <li>study something new</li>
  <li>learn Kotlin</li>
  <li>code Sparkr</li>
</ul>

<p>Each one of those have sub-tasks, and trying to do them without a system to set priorities nor The Sargent™️ is close to impossible. I end up picking up one and sticking to it most of the day completely forgetting of the existence of the other ones.</p>

<h2 id="7-the-rulebook-changes-in-winter-with-less-sun">7. The rulebook changes in winter with less sun</h2>

<p>A seasonal-affective disorder (SAD) light is not optional. It’s mandatory if I want to work the whole day with the big PC. I might feel i have enough energy but nope, if I don’t keep it on most of the day (it’s a small one) then I’m going to feel tired at lot earlier. At least during winter or while living in a dark place, which I am, at the moment.</p>

<h2 id="8-task-lock-awareness-or-excessive-drive-actually-has-a-name">8. Task lock awareness or excessive drive actually has a name</h2>

<p>This is connected with number 1 and number 3 and it’s part of the <a href="https://en.wikipedia.org/wiki/Default_mode_network">default-mode network</a>. That’s an area of the brain ADHDers have overdeveloped, and is in charge among many other things of the voice (or voices) we hear on our minds while we think.</p>

<p>That voice is my scheduler, with it going quiet or having zero independence, I lose the ability to realise I’ve been hours on the same task. It’s also the reason I can’t stand methylphenidate since it makes that voice completely silent while lisdexamphetamine give me more flexibility with it. At the same time, I feel like I’m so used to having “The Sargent” that internally I keep fighting the effects of the pill and if it weren’t for the total emotional regulation I would rather never take these meds.</p>

<h2 id="9-the-role-of-the-time-the-pill-is-taken-and-how-your-reward-system-is-different">9. The role of the time the pill is taken and how your reward system is different</h2>

<p>Going to the gym is a nightmare on Elvanse because it makes me sweat thrice as much and be a lot thirstier. Unfortunately taking it after the gym is impossible, because without it, I simply never leave the bed. Elvanse sets a new baseline for what “minimum energy levels” is required to jumpstart your day. This is why I rather take it and go back to bed, than wait until I’m fully awake and then take it.</p>

<p>While unmedicated, I just wake up and go to the gym. Every task feels the same ( = nothing) until I am actually finishing the task and THEN I get my dopamine reward. I get the motivation to go to the gym from chasing that high, the one I get when I hit the shower and I know I completed my workout, not from working out itself.</p>

<p>With Elvanse, this reward system is heavily modified. When I’m there I’m only there, I forget to even play music on my headphones. When I lift, I compete strongly with what I normally can lift, a lot more than usual. But on Elvanse working out is no longer something I do chasing the joy of having done it, but something that I do chasing the knowledge that I’m killing records. Which means a lot more ego-lifting than usual and I have to <strong>re-calibrate</strong> constantly to remember I’m on Elvanse and my perception of max weight is not what it’s healthy.</p>

<h2 id="10-bullet-time-is-gone">10. Bullet time is gone</h2>

<p>Remember that bullet time thing you got at the beginning on Elvanse, that’s long gone. Not because suddenly time has slowed down, but because you are so long with it that you stop perceiving the difference. The days I do not take the meds, time feels nearly the same too, you do notice it flies quicker but still doesn’t feel as before the meds.</p>

<h2 id="11-still-got-adhd-issues-just-different-ones">11. Still got ADHD issues just different ones</h2>

<ul>
  <li>Procrastination is gone, I can do whatever I want at all times, in exchange I have a harder time to figure what I want.</li>
  <li>I’m getting messier, without a voice in my head reminding me stuff I keep forgetting keys, buying groceries, cleaning the house. This is a common ADHD issue but I don’t have it off meds because I weaponized that internal distracting voice into what I call The Sargent.</li>
  <li>Setting priorities is very hard.</li>
  <li>My time perception is awful now.</li>
  <li>I have working memory issues always on meds, I can’t hold unrelated data in my mind.</li>
</ul>

<h2 id="12-what-happens-on-rest-days-and-why-i-cant-be-more-than-2-days-without-meds">12. What happens on rest days and why I can’t be more than 2 days without meds.</h2>

<p>The positives:</p>

<ul>
  <li>I can hold humongous amount of unrelated data</li>
  <li>The Sargent is back, I don’t need TO-DO lists anymore.</li>
  <li>Global views on source code are easier to maintain.</li>
  <li>I can code in my head.</li>
  <li>I’m a lot more aware of my body and sensations.</li>
  <li>Going to the gym and enjoying it is a lot easier because I’m used to delayed rewards.</li>
</ul>

<p>The negatives:</p>

<ul>
  <li>They all happen in the afternoon</li>
  <li>I’m exhausted, mentally exhausted.</li>
  <li>I’m incredibly hungry, and overeat.</li>
  <li>Noise is a nightmare, especially when there’s too much and is unpredictable. Is like being in twitchy, jumpy mode.</li>
  <li>I can’t sleep, I have no control of my default-mode network. I can only sleep by physical exhaustion.</li>
  <li>I’m very irritable with everyone around me and spending humongous amounts of mental energy trying to not get irritated for things that I know shouldn’t bother me that much.</li>
  <li>I can’t stop yawning.</li>
  <li>Out-of-nowhere anxiety starts crippling in, a baseline anxiety that I had my entire life off-meds and I don’t have in the slightest on meds. Financial worries are the biggest ones crippling in. While on meds, I can focus on finding solutions instead of constantly enumerating the problems in my head.</li>
</ul>

<p>The negatives don’t happen the first day off meds, they start to happen toward the ending of the 2nd day meds free, and they always come with an immediate and huge craving for caffeine.</p>

<h2 id="13-i-tried-splitting-my-dosages-and-it-only-gives-me-anxiety">13. I tried splitting my dosages and it only gives me anxiety</h2>

<p>I have all the crazy chemistry doctor material to split my Elvanse dosages in whatever amounts of mutiple of 10 miligrams. Nothing works. It’s got to be 50mg, any less than 50mg and all I have is anxiety. Only 50 reaches the threshold to build up the necessary dopamine to have more positives than negatives. Thirty only works when I’ve woken up really late and take it in the afternoon.</p>

<p>This has made me consider the option of changing into 30mgs and spending half day off meds, the other half with meds so I get to enjoy my favourite “me” format. But it’s jerky. 30mg works when it’s the pills, when it’s the split dosage, it can keep me awake at night. I’m not going to spend 83€ on 30mg pills.</p>

<h2 id="14-bad-trips">14. Bad trips</h2>

<p>Not all days are perfect, some days I have really really bad effect and the memory issues are even worse. Then the inability to stretch my attention span enough to remember what I’m doing starts to make me feel anxious. The worst day this happened, it literally feels like having early-onset Alzheimer. It took me 3 hours to remember the main task I was doing was moving the shoes from mine and Remi’s bedroom into the entrance shoes cabinet. Can you imagine the desparation of seeing the time fly while you spend it trying to remember what were you doing? You don’t have even short-term memory enough to be able to reach a notepad or Obsidian to write it.</p>

<p>It fucks my short-term memory more than my working one. But, as I said, that’s on the odd bad trip day which usually happen before I decide to take a couple of days off the meds. Those days are usually the reason I’d rather stop the meds altogether or even try the 30mg option. But right now what I’m studying the most is the modifying power of naps since that will dictate in the near future my dosage or even choice of med.</p>

<h2 id="15-why-i-find-cycling-meds-on-and-meds-off-is-so-necessary">15. Why I find cycling meds on and meds off is so necessary?</h2>

<p>Memory. Each state of mind (med/unmed) changes the way my memory works and what I have more easily accessible. I tend to forget the cons of being unmedicated after long time medicated, and the same happens the other way. Both are bad. None are optimal. The solution is <strong>definitely</strong> going to be self-directed cognitive-behavioral therapy (CBT) which I’m already doing every time I write this posts.</p>

<p>The key to figure out my strength, weakness and behavioral patterns I need to change is to figure which ones worked and which ones didn’t. I only get that perspective swapping from one to the other. Everything I learn while medicated is stuff I apply later unmedicated with the hope of eventually stopping meds altogether.</p>

<h1 id="what-ive-learnt-so-far-that-will-eventually-help-me-take-no-meds">What I’ve learnt so far that will eventually help me take no meds.</h1>

<h2 id="1-what-i-thought-were-distractions-were-actually-necessary-aids-to-keep-focus">1. What I thought were distractions were actually necessary aids to keep focus</h2>

<p>I do watch a shitload of TV and can’t do anything without music in the background. The gym is the moment of the day where I go through my “discovery weekly” playlist and classify what I like from what I don’t. I’m normally following the rhythm of the music when I do so. I don’t think about it, I just do. It’s <strong>the background task</strong> that keeps me focused. The passive thing you do without paying much attention to it, that somehow keeps the most distracting inner dialogue from focusing on…distracting me.</p>

<p>On medication watching TV bores me, but because it normally does and I can do it without paying much attention to it. Medicated it becomes the main task and is simply not enough interesting.</p>

<h2 id="2-the-sargent️-has-a-cost">2. The Sargent™️ has a cost</h2>

<p>Maintaining the voice that every couple of minutes reminds me of what I have to do, to take the keys when going out, that the house is filthy, that I need to shower, that the fridge is empty…is the main reason my brain gets exhausted. I cannot turn it off unmedicated, too many years of practice.</p>

<p>But I can refrain from feeding it by writing down what I’ve got to do. This sounds easier than it is, because that voice is capable of sorting out priorities and take quick decisions on the fly, while writing down implies doing all that in advance and learning which tasks shouldn’t need remembering.</p>

<h2 id="3-the-humongous-working-memory-or-internal-blackboard-also-has-a-cost">3. The humongous working memory or internal blackboard also has a cost</h2>

<p>I can code in my head, I can explore solutions without typing a single line. Together with the sargent this has another humongous costs and is probably the reason I get mentally exhausted by the afternoon. While I’m fine I don’t even feel any effort in doing this I just keep getting progressively tired until at some point of the day, its gets a life of its own and feels like being enclosed in the Architect room of The Matrix with all the screens as the same volume claiming for your attention.</p>

<p>Again, the solution is of course to use it less, but how do you do that when it feels effortless to use. It’s easy in the case of the sargent, but maintaining humongous abstractions gives you a lot of speed. Is like running a full OS from RAM instead of hard drive.</p>

<p>This is the main thing I need to fix to be pill free. And so far all I’ve got is that I don’t have access to either while medicated. e.g: the abstractions on meds can be just as large but they are thinner, more specific, less global. Without meds I don’t need to re-read this entire article to avoid repeating myself, I would know what I wrote, while medicated I do because each paragraph is a different issue.</p>

<p>And none of that is giving me yet a clue on how to avoid using the full available bandwidth unmedicated. So far all I got is that I can reset it by having a nap.</p>

<h2 id="4-napping-is-the-key-to-everything">4. Napping is the key to everything</h2>

<p>When I was a kid, something that I can only remember on meds because it expands the access and reach of my long-term memory, is that every afternoon I could spend ~4 hours sitting on sofa with nothing but myself, I wasn’t bored, nor I had my eyes closed, even though I surely would have loved to do so and fall sleep and indeed sometimes I did, I was just letting go.</p>

<p>When I reached exhaustion and was overwhelmed by my own brain I just relaxed by becoming an spectator. There was nothing to fear, I’ve always known in the afternoon that I would feel exhausted until I got my second wind. What I didn’t know until the diagnose is that it was untreated ADHD the reason.</p>

<p>So I would just sit comfortably in the nicest sofa around in my home and daydream until I eventually had a short nap or the exhaustion vanished.</p>

<p>The thing is on meds, I can just fall anytime. I’m definitely planning to do so after finishing this awful thing that is already consuming 6 hours of my time.
<img src="/assets/images/2026-01-image-4.png" alt="Image" />
But without meds I cannot. So I need to figure how to transfer that skill from medicated to unmedicated. Because if I can have a nap then I can be unmedicated and in the right side of my ADHD symptoms the whole day.</p>

<h2 id="5-memory-might-not-be-erratic-just-different">5. Memory might not be erratic just different</h2>

<p>Remember what I’ve just written, on medication I can remember things from the very-long-ago past better while my short-term and working memory are normally limited, while other days the short-term is kind of frustratingly non-existent.</p>

<p>But what if that’s what’s normal.</p>

<p>I have the theory that I’ve somehow specialised my brain in recycling the space for long-term memory in exchange for a bigger daily working memory. I notice same I can remember with 10x more detail what happened around the city where I live, street names, where to find stuff, etc from the most immediate past, I erase the stuff I don’t care about from the far past very quickly while everybody around me can remember stuff from their childhood that I do not and for them is not normal I cannot, while for me, it is. e.g.: each generation has a cartoon they were addicted to, mine was Dragon Ball. I can’t barely remember anything about it, not even names, while Adrian that is my same generation can tell me entire plotlines. And I do know I was addicted to those cartoons but I know it is information I did not care at all about storing.</p>

<p>I’m yet to meet someone else that can semi-consciously decide what information to delete.</p>

<h2 id="6-emotional-disregulation-is-100-tied-to-mental-exhaustion">6. Emotional disregulation is 100% tied to mental exhaustion</h2>

<p>Once I cannot control my inner monologue, I can’t do anything at will other than rest. So my body becomes this 1-goal zombie that only wants one thing: to rest. And anyone standing in the way between me and rest <strong>they’re going to get run over</strong> and there’s nothing I can do about it other than subtract myself from their presence as I’m a ticking bomb with no control over what they say to be left alone and sleep.</p>

<h2 id="7-decision-fatigue-is-a-big-issue">7. Decision fatigue is a big issue</h2>

<p>The Sargent does spend a humongous amount of energy moving priorities. In this sense there’s a feedback loop between it and the abuse of the bandwidth of my working memory. You can take better decisions when you have as many possibilities displayed in your mind’s eye as possible.</p>

<p>The solution is to delegate. To who? To me in the past. Decide ahead of the future, in small chunks. Avoid my daily routine to have menial decisions. Decisions are decisions big or small, the less you take, the less decision fatigue. If I can get rid off the smaller ones, then I’m further from suffering it.</p>

<p>Also, just doing what’s right instead of what’s optimal helps. What’s optimal never gets an immediate answer, what’s right always does.</p>

<h2 id="8-measure-times">8. Measure times</h2>

<p>I’m making tasks that take really little time a lot longer by overthinking them, I should be measuring how long do they take me and challenge that timing.</p>

<h2 id="9-small-increments">9. Small increments</h2>

<p>Only small changes stick but they have to be so simple and dumbed down that they are effortless. Like, instead of focusing on trying to wake up to go to the gym, focus on not taking the pill at 7am independently of leaving the bed or carrying on sleeping.</p>

<h1 id="conclusion">Conclusion</h1>

<p>In a nutshell meds-free life is going to be:</p>

<ul>
  <li>Learning to not abuse and instead protect my bandwidth, by using it as little as possible, and changing ways I do stuff to save it for when I actually need it. e.g.: actually writing to-do lists.</li>
  <li>avoid analysis paralysis and decision fatigue as anything involving this, will undoubtedly make me think globally.</li>
  <li>Use meds as a tool to build new systems, then lean on external scaffolding. Don’t rely on internal memory or the “Sargent” voice.</li>
  <li>Cycle between medicated and unmedicated states to test what works and learn from each mode.</li>
</ul>

<p>Manage mental energy smarter: naps, pacing, and avoiding afternoon crashes that make meds <strong>be</strong> necessary.</p>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="ADHD" /><summary type="html"><![CDATA[My experience with ADHD and what has been working so far ever since starting on medication. This episode is about the usage of Elvanse long-term.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1675524375084.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1675524375084.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A Coruña, primera zona tensionada de Galicia: guía práctica de precios, contratos y obligaciones</title><link href="https://blog.maikel.dev/2025/10/21/a-coruna-primera-zona-tensionada-de-galicia-guia-practica-de-precios-contratos-y-obligaciones.html" rel="alternate" type="text/html" title="A Coruña, primera zona tensionada de Galicia: guía práctica de precios, contratos y obligaciones" /><published>2025-10-21T00:00:00+00:00</published><updated>2025-10-21T00:00:00+00:00</updated><id>https://blog.maikel.dev/2025/10/21/a-coruna-primera-zona-tensionada-de-galicia-guia-practica-de-precios-contratos-y-obligaciones</id><content type="html" xml:base="https://blog.maikel.dev/2025/10/21/a-coruna-primera-zona-tensionada-de-galicia-guia-practica-de-precios-contratos-y-obligaciones.html"><![CDATA[<p>A Coruña se ha convertido en la <strong>primera ciudad gallega declarada zona de mercado residencial tensionado</strong> por el <strong>alto precio del alquiler</strong>.
La medida, enmarcada en la <strong>Ley 12/2023 por el Derecho a la Vivienda</strong>, busca frenar la escalada de precios y mejorar el acceso a la vivienda, especialmente en barrios donde el coste del alquiler se ha disparado por encima del 30 % de los ingresos familiares.</p>

<p>A partir de ahora, <strong>inquilinos y propietarios deberán adaptarse a un nuevo marco legal</strong> que incluye un índice de precios de referencia, límites para grandes tenedores y beneficios fiscales para los pequeños arrendadores que ajusten las rentas.</p>

<h2 id="-el-nuevo-sistema-estatal-de-referencia-de-precios-de-alquiler">📊 El nuevo Sistema Estatal de Referencia de Precios de Alquiler</h2>

<p>Una de las principales herramientas de esta regulación es el <strong>Sistema Estatal de Referencia de Precios de Alquiler de Vivienda</strong>, creado por el Ministerio de Vivienda.
Esta aplicación oficial permite conocer el <strong>precio de referencia</strong> de una vivienda a partir de datos tributarios sobre arrendamientos habituales, considerando superficie, ubicación, antigüedad, estado o eficiencia energética.
<strong>💡 Ejemplo:</strong> para un piso de 90 m² en A Coruña, el precio de referencia puede variar desde <strong>377 € en Agra do Orzán</strong> hasta <strong>825 € en la plaza de Lugo</strong>.</p>

<h2 id="-cuadro-de-precios-orientativos-por-zonas-vivienda-tipo-de-90-m">📍 Cuadro de precios orientativos por zonas (vivienda tipo de 90 m²)</h2>
<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">**ATENCION INQUILINOS**
En esta web si metes la dirección de tu casa te dice cuanto te pueden cobrar como máximo. [https://serpavi.mivau.gob.es/](https://serpavi.mivau.gob.es/)</div></div>
<table>
<thead>
<tr>
<th>Zona / Barrio</th>
<th>Rango de precio (€)</th>
<th>€/m² aprox.</th>
<th>Comentario</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Plaza de Lugo (Centro – Ensanche)</strong></td>
<td>616 – 825 €</td>
<td>6,8 – 9,2 €/m²</td>
<td>Máxima presión de precios; escasa oferta y alta demanda.</td>
</tr>
<tr>
<td><strong>Calle San Andrés (Centro histórico)</strong></td>
<td>555 – 746 €</td>
<td>6,2 – 8,3 €/m²</td>
<td>Zona prime, mucha rotación y presencia de alquiler turístico.</td>
</tr>
<tr>
<td><strong>Monte Alto – Ronda de Monte Alto</strong></td>
<td>432 – 544 €</td>
<td>4,8 – 6,0 €/m²</td>
<td>En auge, buena relación precio-ubicación.</td>
</tr>
<tr>
<td><strong>Riazor – Ciudad Escolar</strong></td>
<td>509 – 707 €</td>
<td>5,6 – 7,8 €/m²</td>
<td>Alta demanda, especialmente en pisos pequeños y reformados.</td>
</tr>
<tr>
<td><strong>Os Castros – Castrillón</strong></td>
<td>419 – 565 €</td>
<td>4,6 – 6,3 €/m²</td>
<td>Alternativa media con precios más contenidos.</td>
</tr>
<tr>
<td><strong>Agra do Orzán</strong></td>
<td>377 – 536 €</td>
<td>4,2 – 5,9 €/m²</td>
<td>Zona popular y densamente poblada; índice más bajo del centro urbano.</td>
</tr>
<tr>
<td><strong>Sagrada Familia</strong></td>
<td>319 – 468 €</td>
<td>3,5 – 5,2 €/m²</td>
<td>Mercado asequible y tradicional.</td>
</tr>
<tr>
<td><strong>Novo Mesoiro</strong></td>
<td>444 – 528 €</td>
<td>4,9 – 5,8 €/m²</td>
<td>Barrio joven y en expansión, con buena oferta familiar.</td>
</tr>
</tbody>
</table>
<p><strong>📈 El precio medio actual del mercado en A Coruña ronda los 730 €/mes</strong>, según el Instituto Galego de Vivenda e Solo (IGVS).</p>

<p>En portales como Idealista, <strong>dos tercios de los pisos en oferta superan los 900 €</strong>, lo que confirma la brecha entre la realidad del mercado y el índice estatal.</p>

<h2 id="️-quién-debe-aplicar-el-índice-de-referencia">⚖️ Quién debe aplicar el índice de referencia</h2>

<p>Según la Ley de Vivienda, el <strong>índice no afecta a todos los propietarios por igual</strong>:</p>

<ul>
  <li>Grandes tenedores (dueños de más de 10 viviendas) deben ajustarse obligatoriamente al índice al firmar un nuevo contrato en una zona tensionada.</li>
  <li>Pequeños propietarios solo deben hacerlo si:
    <ul>
      <li>Firman un nuevo contrato en una zona tensionada, y</li>
      <li>El piso no ha estado alquilado en los últimos 5 años.</li>
    </ul>
  </li>
</ul>

<p>El Ministerio no fija un precio único, sino una <strong>horquilla mínima y máxima</strong>, que permite flexibilidad según las características del inmueble (planta, antigüedad, certificación energética, etc.).</p>

<h2 id="-reglas-clave-de-los-contratos-en-zonas-tensionadas">🏠 Reglas clave de los contratos en zonas tensionadas</h2>

<h3 id="1-limitación-de-rentas">1. Limitación de rentas</h3>

<ul>
  <li>Si el piso ya estaba alquilado en los últimos 5 años, el nuevo contrato no puede superar la renta anterior actualizada con el índice permitido.</li>
  <li>Si nunca se alquiló o lleva más de 5 años vacío, el precio máximo será el del índice estatal correspondiente.</li>
</ul>

<h3 id="2-actualización-anual">2. Actualización anual</h3>

<ul>
  <li>En 2025, la subida máxima será del 3 %.</li>
  <li>A partir de 2026, se aplicará un nuevo índice (aún pendiente de definir).</li>
</ul>

<h3 id="3-duración-y-prórrogas">3. Duración y prórrogas</h3>

<ul>
  <li>Contratos de 5 años (propietarios particulares) o 7 años (personas jurídicas).</li>
  <li>En zonas tensionadas, el inquilino puede pedir una prórroga adicional de hasta 3 años, salvo causa justificada de necesidad del propietario.</li>
</ul>

<h3 id="4-registro-autonómico">4. Registro autonómico</h3>

<ul>
  <li>Todos los contratos deben inscribirse en el registro de arrendamientos de Galicia.</li>
  <li>La Xunta podrá inspeccionar y sancionar incumplimientos del límite de precios.</li>
</ul>

<h2 id="-beneficios-fiscales-e-infracciones">💰 Beneficios fiscales e infracciones</h2>

<h3 id="incentivos">Incentivos</h3>

<p>Los propietarios que ajusten el precio a los límites y ofrezcan estabilidad pueden obtener <strong>bonificaciones de hasta el 90 % en el IRPF</strong> por los ingresos del alquiler, siempre que cumplan con los requisitos de duración y referencia del índice.</p>

<h3 id="sanciones">Sanciones</h3>

<p>El incumplimiento de la normativa puede acarrear consecuencias graves:</p>

<ul>
  <li>Multas de hasta 60.000 € por infracciones muy graves.</li>
  <li>Nulidad de las cláusulas abusivas.</li>
  <li>Pérdida de beneficios fiscales aplicados indebidamente.</li>
  <li>Inspección y sanción administrativa si se detectan rentas fuera del índice.</li>
</ul>

<h2 id="-impacto-en-el-mercado-de-a-coruña">🔍 Impacto en el mercado de A Coruña</h2>

<p><strong>Efectos esperados:</strong></p>

<ul>
  <li>Mayor transparencia en los precios.</li>
  <li>Incentivo para rehabilitar viviendas vacías.</li>
  <li>Estabilidad para inquilinos y previsibilidad para propietarios.</li>
</ul>

<p><strong>Riesgos a vigilar:</strong></p>

<ul>
  <li>Reducción temporal de la oferta.</li>
  <li>Aumento del alquiler turístico o de habitaciones.</li>
  <li>Desplazamiento de la demanda a municipios cercanos (Oleiros, Culleredo, Arteixo).</li>
</ul>

<h2 id="-conclusión">🧭 Conclusión</h2>

<p>Con la declaración de A Coruña como zona tensionada, la ciudad entra en una nueva etapa del mercado del alquiler.</p>

<p>La regulación busca equilibrio entre protección al inquilino y sostenibilidad para los propietarios, pero el éxito dependerá de su aplicación práctica y del control efectivo por parte de las administraciones.</p>

<p><strong>En definitiva:</strong> la vivienda en A Coruña no solo se regulará por la oferta y la demanda, sino también por un <strong>marco normativo que fija límites, incentiva la rehabilitación y penaliza los abusos</strong>.</p>

<p>Un cambio estructural que marcará los próximos años del mercado inmobiliario gallego.</p>
<h1 id="mas-información">Mas información</h1>

<ul>
  <li>Página de la cual copie el contenido porque yo lo valgo y porque nada toca mas los huevos que una página creyendo que puede decidir que yo no pueda copiar y pegar.</li>
  <li>Precios cogidos de este artículo de La Opinión de Coruña</li>
  <li>Tambien de esta otra</li>
</ul>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="real-estate" /><category term="Spain" /><category term="Coruña" /><summary type="html"><![CDATA[En este artículo te explico, de forma clara y práctica, qué significa exactamente que A Coruña sea zona tensionada, qué limitaciones introduce y cómo puede afectarte tanto si ya tienes una vivienda alquilada como si planeas ponerla en el mercado.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1679432494197.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1679432494197.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Loading all environment variables from Bitwarden on terminal launch without noticeable lag</title><link href="https://blog.maikel.dev/2025/10/21/loading-all-environment-variables-from-bitwarden-on-terminal-launch-without-noticeable-lag.html" rel="alternate" type="text/html" title="Loading all environment variables from Bitwarden on terminal launch without noticeable lag" /><published>2025-10-21T00:00:00+00:00</published><updated>2025-10-21T00:00:00+00:00</updated><id>https://blog.maikel.dev/2025/10/21/loading-all-environment-variables-from-bitwarden-on-terminal-launch-without-noticeable-lag</id><content type="html" xml:base="https://blog.maikel.dev/2025/10/21/loading-all-environment-variables-from-bitwarden-on-terminal-launch-without-noticeable-lag.html"><![CDATA[<h1 id="the-problem-im-trying-to-solve">The problem I’m trying to solve</h1>

<p>If you use Bitwarden you might have tried using <a href="https://bitwarden.com/help/cli/">Bitwarden CLI</a> which is a nightmare of a tool clearly not fit for purpose.</p>

<p>The main problem with it is that it takes between 1 to 5 seconds for every value you retrieve. The secondary problem is that unlike any other official Bitwarden implementation it is impossible to remain logged in without writing your own scripts to store BW_SESSION (logging with api credentials), pass it between shell sessions or make it universal and still make it so that it tests it works and relogs again if the session token has expired. Considering this again costs between 1 to 5 seconds it is too much time to waste whenever you open a new terminal.</p>

<p>I use <a href="https://fishshell.com/">Fish shell</a>, it’s been my favourite shell for more than ten years, it does stuff that all other shells should be doing now without plugins required. In Fish shell when you make a universal environment variable with <code class="language-plaintext highlighter-rouge">set -U VARIABLE</code>, it doesn’t magically get into the ether or stored in memory, it gets writen on the filesystem on <code class="language-plaintext highlighter-rouge">$HOME/.config/fish/fish_variables</code> and I definitely don’t want that there.</p>

<p>So we have two problems, on one side the official bitwarden cli takes ages with any command, and on the other side I don’t want to keep the session alive by writing the token to a file anywhere on my system.</p>

<h1 id="rbw-entered-the-room">RBW entered the room</h1>

<p>The program <code class="language-plaintext highlighter-rouge">rbw</code> is a command-line tool that does the same job as Bitwarden-CLI albeit very **very **differently. To begin with the commands are not the same, and the way you search for stuff is not the same either. But the most important value of <code class="language-plaintext highlighter-rouge">rbw</code> is that it maintains the connection logged unlike the official CLI tool.</p>

<p>It has a small issue, which is that it doesn’t accept <a href="https://github.com/doy/rbw/issues/292#issuecomment-3425564942">Webauth/FIDO2 as 2FA right now</a> but you can bypass this issue enabling any of the other accepted 2FA ways. In fact I enabled TOTP and it worked beautifully, I was asked for the temporal number combo only once the first time. You can use bitwarden itself as TOTP generator for this since you already have other login mechanisms for it.</p>

<p>That done, one problem was solved, login with <code class="language-plaintext highlighter-rouge">rbw</code> is very easy after installing it, if you use Bitwarden’s official vault, same as I do, do not forget the <code class="language-plaintext highlighter-rouge">register</code> command as explained in the <a href="https://github.com/doy/rbw">rbw repo</a> also if you use Nixos add <code class="language-plaintext highlighter-rouge">pinsentry-tty</code> to your packages, not <code class="language-plaintext highlighter-rouge">pinsentry</code> not any GUI version, you want the one for the terminal . Then do:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># To configure it</span>
rbw config <span class="nb">set </span>email your@ema.il
<span class="c"># Lock timeout in seconds, I prefer 16 hours, covers the time I'm awake</span>
rbw config <span class="nb">set </span>lock_timeout <span class="si">$(</span>math <span class="s2">"60*60*16"</span><span class="si">)</span>
<span class="c"># To log in</span>
rbw login
</code></pre></div></div>

<p>And that’s it. You don’t need to do anything else. Any changes in config will ask you to login again the next time you try to get anything out of Bitwarden. With this we’ve solved the second problem, keeping environment variables anywhere on our system to stay connected.</p>

<h1 id="next-issue-rbw-speed-has-limits">Next issue: RBW speed has limits</h1>

<p>So imagine I want to set up a function called <code class="language-plaintext highlighter-rouge">load_bitwarden_vars</code> that I call at the very end of <code class="language-plaintext highlighter-rouge">config.fish</code> so something like this</p>

<pre><code class="language-fish">function load_bitwarden_vars
  set -gx GITHUB_CLIENT_ID $(rbw get github -f client_id)
  set -gx GITHUB_CLIENT_SECRET $(rbw get github -f client_secret)
  set -gx ZT_TOKEN $(rbw get zerotier -f token)
  set -gx ZT_NWID $(rbw get zerotier -f network_id)
end
</code></pre>

<p>The <code class="language-plaintext highlighter-rouge">rbw</code> tool has the advantage over <code class="language-plaintext highlighter-rouge">bw</code> that it is a lot faster retrieving data, milliseconds, not seconds. But the more calls you do to it during start up of fish shell the slower it gets for you to see the shell prompt whenever you open a window. This is unwieldly. I don’t have 4 vars, I have plenty more of them. After filling up my <code class="language-plaintext highlighter-rouge">load_bitwarden_vars.fish</code> file containing the function this took a very **unacceptable **long time.</p>

<p>I came up with the idea of putting each var directly as an alias or abbreviation of the tool it uses them. So, e.g: vault became either</p>

<pre><code class="language-fish"># option 1
abb --add vault "TOKEN=$(rbw get...) vault"
# option 2
alias vault "TOKEN=$(rbw get...) /run/current-system/sw/bin/vault"
</code></pre>

<p>Yet alias has the issue of needing the whole vault path (Nixos) otherwise it becomes an infinite loop and Fish doesn’t allow it. And both have the major issue of computing the values when Fish launches so very slow.</p>

<h1 id="the-solution">The solution</h1>

<p>One call, just one. It doesn’t matter how big is the item, with just one call it still takes less than a second. So I created an item called <code class="language-plaintext highlighter-rouge">tokens</code> in Bitwarden, I made it as a note, but it really doesn’t matter if you store it as a login, identity or anything else because <code class="language-plaintext highlighter-rouge">rbw</code> doesn’t care of what it is when it retrieves it with <code class="language-plaintext highlighter-rouge">rbw get tokens</code> all that matters is the custom fields I created.</p>

<p>For each one of the variables I want to retrieve I created a hidden text field, with the exact name of the variable. For conveniency I used Bitwarden-desktop for this but you could easily use the CLI tool or <code class="language-plaintext highlighter-rouge">rbw</code> itself to do so. I just had to do too much copy and paste from different places so prefered the visual GUI.
<img src="/assets/images/2025-10-image-2.png" alt="Image" />
The elegance of this approach is that I get to name all vars directly on Bitwarden as custom-fields, no sign of them in my fish config files.</p>

<p>So now to get Fish to load them type <code class="language-plaintext highlighter-rouge">funced load_bitwarden_vars</code> and add this to it</p>

<pre><code class="language-fish">function load_bitwarden_vars
  set TOKENS $(rbw get tokens --raw)
  for pair in (printf "%s" "$TOKENS" | jq -r '.fields[] | "\(.name)=\(.value)"')
    set -l parts (string split -m1 = $pair)
    set -gx $parts[1] $parts[2]
  end
end
</code></pre>

<p>Then <code class="language-plaintext highlighter-rouge">funcsave load_bitwarden_vars</code> to store it in <code class="language-plaintext highlighter-rouge">$HOME/.config/fish/functions/load_bitwarden_vars.fish</code> and then add at the end of <code class="language-plaintext highlighter-rouge">$HOME/.config/fish/config.fish</code> the function name <code class="language-plaintext highlighter-rouge">load_bitwarden_vars</code> to run it after everything else of your config has loaded. In my case being at the end was quite important as I have modifiers for <code class="language-plaintext highlighter-rouge">PATH</code> in this file.</p>

<p>Now to test it fully lock your vault with <code class="language-plaintext highlighter-rouge">rbw lock</code> and close your terminal window. Open a newone and you should see 👇
<img src="/assets/images/2025-10-image-3.png" alt="Image" />
Once you log in, close the shell and open it again. This time it doesn’t ask for anything, you’ll notice a very small lag but one you can <strong>comfortably</strong> live with. In fact try to open multiple terminal windows.</p>

<p>You’ve tested both cases, being logged and locked. As you see it works.</p>

<p>**Problem solved! **🥳</p>

<h1 id="how-it-works">How it works</h1>

<p>It’s pretty simple actually, it uses the speed of <code class="language-plaintext highlighter-rouge">rbw</code> for any given item, combined with JQ to set the variables without making them universal so they don’t get written to disk.</p>

<ol>
  <li>We take the whole output as json of the item tokens from Bitwarden and assign it to the local variable TOKENS</li>
  <li>With JQ we extract from the fields array on TOKENS each one of the pairs of name and value  custom fields we created. Format them as lines name=value</li>
  <li>With a for loop we iterate over each one of those lines and assign them to pair</li>
  <li>The inner content of the loop asigns each part of part to the corresponding local variables name and value.</li>
  <li>The final line inside the loop assigns the globally exported variable. So that shell window and any subshells get the values.</li>
</ol>

<p>I’m sure it can be improved and if you come up with ideas feel free to provide me some feedback. I’m not entirely sure of the difference between the types of variables. I mean universal, local and function environment variables are pretty self-explanatory but exported and global are not that clear. The manual says exported makes them available to child processes, but how’s that different from global? Feel free to share some light to me about this in Mastodon.</p>

<h1 id="useful-links">Useful links</h1>

<ul>
  <li>Fish shell scope in the Fish manual, very important to understand how set -U works.</li>
  <li>RBW repo page in Github.</li>
</ul>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="security" /><category term="tools" /><category term="Bitwarden" /><category term="NixOS" /><summary type="html"><![CDATA[Solving the major issue of Bitwarden being incredibly slow to load environment variables by combining rbw, jq and fish shell]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1614064641938.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1614064641938.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Particularidades del mercado inmobiliario de A Coruña</title><link href="https://blog.maikel.dev/2025/10/21/particularidades-del-mercado-inmobiliario-de-a-coruna.html" rel="alternate" type="text/html" title="Particularidades del mercado inmobiliario de A Coruña" /><published>2025-10-21T00:00:00+00:00</published><updated>2025-10-21T00:00:00+00:00</updated><id>https://blog.maikel.dev/2025/10/21/particularidades-del-mercado-inmobiliario-de-a-coruna</id><content type="html" xml:base="https://blog.maikel.dev/2025/10/21/particularidades-del-mercado-inmobiliario-de-a-coruna.html"><![CDATA[<h3 id="1modelo-urbano-e-histórico">1.<strong>Modelo urbano e histórico</strong></h3>

<ul>
  <li>Desde los años 80, A Coruña presenta una división socioespacial marcada:
    <ul>
      <li>Centro histórico y Ensanche → clases medias-altas y procesos de rehabilitación.</li>
      <li>Barrios periféricos o satélites (Novo Mesoiro, Matogrande, Someso, Los Rosales, Ventorrillo, Adormideras) → creados en los 90-2000 bajo el mandato de Francisco Vázquez, con carencias de servicios, movilidad y cohesión urbana.</li>
    </ul>
  </li>
  <li>La expansión periférica se hizo a costa de <strong>abandonar el centro</strong>, lo que facilitó más tarde la <strong>gentrificación</strong> y el <strong>encarecimiento de la vivienda rehabilitada</strong>.</li>
</ul>

<hr />
<h3 id="2estructura-y-dinámica-del-mercado">2.<strong>Estructura y dinámica del mercado</strong></h3>

<ul>
  <li>Oferta muy reducida de alquiler y exceso de vivienda vacía o en mal estado.</li>
  <li>Se mantiene una preferencia cultural por la propiedad, pero la compra se volvió inalcanzable para gran parte de la población desde los 2000.</li>
  <li>Los precios del alquiler subieron de manera constante entre 2014 y 2022, llegando a ser de los más altos de Galicia, especialmente en barrios céntricos como Papagayo o Monte Alto.</li>
</ul>
<hr />

<h3 id="3gentrificación-urbana-y-comercial">3.<strong>Gentrificación urbana y comercial</strong></h3>

<ul>
  <li>A Coruña ha vivido varias fases de gentrificación:
    <ul>
      <li><strong>Fase 1 (2000–2008)</strong>: rehabilitación en Ciudad Vieja y Paseo del Parrote; transformación del mercado de la Plaza de Lugo con la entrada de marcas de lujo e Inditex; encarecimiento del centro.</li>
      <li><strong>Fase 2 (2008–2015)</strong>: crisis del ladrillo → precarización, pero también consolidación de la “ciudad creativa”, con llegada de población joven con mayor renta.</li>
      <li><strong>Fase 3 (2015–actualidad)</strong>: expansión de la gentrificación hacia Monte Alto, San Agustín y Curros Enríquez, ligada a rehabilitación y turismo urbano.</li>
    </ul>
  </li>
</ul>

<hr />
<h3 id="4el-efecto-inditex">4.<strong>El “efecto Inditex”</strong></h3>

<ul>
  <li>Inditex no domina toda la economía urbana, pero tiene efectos indirectos claros:
    <ul>
      <li><strong>Simbolismo y atracción de talento</strong>: la empresa genera una imagen de prosperidad que influye en la percepción de la ciudad.</li>
      <li><strong>Cambio en la demanda residencial</strong>: los trabajadores de Inditex son considerados inquilinos “ideales”, percibidos como solventes y estables, lo que margina al precariado local.</li>
      <li><strong>Contribución indirecta a la gentrificación</strong>: zonas céntricas se adaptan al estilo de vida de esa “clase creativa” y a la economía del consumo cultural y turístico.</li>
    </ul>
  </li>
  <li>La autora y los expertos advierten que A Coruña depende en exceso del “monocultivo Inditex” (hasta un 45% de la productividad local), lo que supone un riesgo futuro.</li>
</ul>

<hr />
<h3 id="5problemas-estructurales">5.<strong>Problemas estructurales</strong></h3>

<ul>
  <li>Escasez de vivienda pública y políticas de alquiler social.</li>
  <li>Desigualdad territorial: distritos como Monte Alto y Torre concentran precios más altos; Los Mallos o Agra del Orzán muestran menor renta y envejecimiento poblacional.</li>
  <li>Dependencia del sector inmobiliario y de la plusvalía del suelo en los presupuestos municipales.</li>
  <li>Presión del turismo y auge de pisos turísticos, que agravan la exclusión residencial.</li>
</ul>
<hr />

<h3 id="-conclusión-general">🧭 Conclusión general</h3>

<p>El mercado inmobiliario coruñés combina tres tensiones:</p>

<ol>
  <li>Desequilibrio centro-periferia → abandono del centro seguido de gentrificación.</li>
  <li>Influencia de Inditex → simbólica y material, reconfigura quién puede vivir y consumir en el centro.</li>
  <li>Débil intervención pública → políticas de vivienda insuficientes para frenar exclusión o especulación.</li>
</ol>

<p>En palabras de la tesis, A Coruña es hoy <strong>una “ciudad creativa gentrificada”</strong>, donde el acceso a la vivienda depende tanto de los ingresos como del tipo de empleo y de la identidad social del inquilino.</p>

<hr />
<h1 id="bibliografía">Bibliografía</h1>

<ul>
  <li>Maravillosa tesis doctoral de 400 y pico páginas de Julia Nogueira Dominguez https://ruc.udc.es/entities/publication/8a8f26be-5d80-483e-8241-50f59c7aa920</li>
</ul>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="real-estate" /><category term="Spain" /><category term="Coruña" /><summary type="html"><![CDATA[Encontré una tesis de 400 y pico páginas sobre la influencia de Inditext en el mercado inmobiliario de Coruña pero lo mas maravilloso de la tesis no es inditex sino como desgrana en 400 páginas las particularidades de esta ciudad. Aqui un resumen.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1560253451.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1560253451.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using HAProxy for Ngrok-style domain tunneling in Nixos with SSL self-renewal enabled</title><link href="https://blog.maikel.dev/2025/10/14/using-haproxy-for-ngrok-style-domain-tunneling-in-nixos-with-ssl-self-renewal-enabled.html" rel="alternate" type="text/html" title="Using HAProxy for Ngrok-style domain tunneling in Nixos with SSL self-renewal enabled" /><published>2025-10-14T00:00:00+00:00</published><updated>2025-10-14T00:00:00+00:00</updated><id>https://blog.maikel.dev/2025/10/14/using-haproxy-for-ngrok-style-domain-tunneling-in-nixos-with-ssl-self-renewal-enabled</id><content type="html" xml:base="https://blog.maikel.dev/2025/10/14/using-haproxy-for-ngrok-style-domain-tunneling-in-nixos-with-ssl-self-renewal-enabled.html"><![CDATA[<p>I love accessing computers from elsewhere yet hate Ngrok, Tailscale and Zrok with passion so I made a post not long ago about <a href="/pagekite-with-custom-domain-in-nixos-tunneling-without-ngrok-for-free/">how to self-host Pagekite</a> to be able to use your own domain names for this. The problem is how ugly it made my configuration.nix files and how I didn’t manage to write an auto-renewal script. Today I’ve figured how using ACME. HAProxy seems to have the same issue of wanting a single file for all keys but I managed to bypass that one here.</p>

<h1 id="configurationnix">Configuration.nix</h1>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Assuming configuration.nix</span>
<span class="p">{</span>
  <span class="c"># Rest of your file</span>
  <span class="nv">imports</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c"># Rest of your imports</span>
    <span class="sx">./haproxy-acme.nix</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="server-haproxy-acmenix">Server: haproxy-acme.nix</h1>

<p>Change <code class="language-plaintext highlighter-rouge">serverNames</code> for whatever list of servers you want to add, don’t forget to manually edit the <code class="language-plaintext highlighter-rouge">services.haproxy.config</code> configuration too for your added servernames without SSL, in my case is my zerotier one, accessible on my zerotier network.</p>

<p>The <code class="language-plaintext highlighter-rouge">acme_email</code>variable should point to your var.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">config</span><span class="p">,</span> <span class="nv">pkgs</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="kd">let</span>
  <span class="nv">acme_email</span> <span class="o">=</span> <span class="s2">"acme@maikel.dev"</span><span class="p">;</span>
  <span class="c"># Map domains to their backend host:port</span>
  <span class="nv">serverBackends</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"something_one.maikeladas.es"</span> <span class="o">=</span> <span class="s2">"127.0.0.1:4000"</span><span class="p">;</span> <span class="c"># Phoenix dev server.</span>
    <span class="s2">"something_else.maikeladas.es"</span> <span class="o">=</span> <span class="s2">"thinkpad.zerotier:8080"</span><span class="p">;</span> <span class="c">#Zerotier URL</span>
  <span class="p">};</span>
  <span class="nv">serverNames</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">attrNames</span> <span class="nv">serverBackends</span><span class="p">;</span>
  <span class="c"># ACLs</span>
  <span class="nv">aclLines</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">concatStringsSep</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span> <span class="p">(</span><span class="kr">map</span> <span class="p">(</span><span class="nv">d</span><span class="p">:</span>
    <span class="kd">let</span> <span class="nv">aclName</span> <span class="o">=</span> <span class="s2">"host_</span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">replaceStrings</span> <span class="p">[</span> <span class="s2">"."</span> <span class="p">]</span> <span class="p">[</span> <span class="s2">"_"</span> <span class="p">]</span> <span class="nv">d</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="kn">in</span> <span class="s2">"  acl </span><span class="si">${</span><span class="nv">aclName</span><span class="si">}</span><span class="s2"> hdr(host) -i </span><span class="si">${</span><span class="nv">d</span><span class="si">}</span><span class="s2">"</span>
  <span class="p">)</span> <span class="nv">serverNames</span><span class="p">);</span>
  <span class="nv">redirectLines</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">concatStringsSep</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span> <span class="p">(</span><span class="kr">map</span> <span class="p">(</span><span class="nv">d</span><span class="p">:</span>
    <span class="s2">"  http-request redirect scheme https code 301 if host_</span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">replaceStrings</span> <span class="p">[</span> <span class="s2">"."</span> <span class="p">]</span> <span class="p">[</span> <span class="s2">"_"</span> <span class="p">]</span> <span class="nv">d</span><span class="si">}</span><span class="s2">"</span>
  <span class="p">)</span> <span class="nv">serverNames</span><span class="p">);</span>
  <span class="c"># use_backend lines</span>
  <span class="nv">backendLines</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">concatStringsSep</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span> <span class="p">(</span><span class="kr">map</span> <span class="p">(</span><span class="nv">d</span><span class="p">:</span>
    <span class="kd">let</span>
      <span class="nv">aclName</span> <span class="o">=</span> <span class="s2">"host_</span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">replaceStrings</span> <span class="p">[</span> <span class="s2">"."</span> <span class="p">]</span> <span class="p">[</span> <span class="s2">"_"</span> <span class="p">]</span> <span class="nv">d</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
      <span class="nv">backendName</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">replaceStrings</span> <span class="p">[</span> <span class="s2">"."</span> <span class="p">]</span> <span class="p">[</span> <span class="s2">"_"</span> <span class="p">]</span> <span class="nv">d</span><span class="p">;</span>
    <span class="kn">in</span> <span class="s2">"  use_backend </span><span class="si">${</span><span class="nv">backendName</span><span class="si">}</span><span class="s2">_app if </span><span class="si">${</span><span class="nv">aclName</span><span class="si">}</span><span class="s2">"</span>
  <span class="p">)</span> <span class="nv">serverNames</span><span class="p">);</span>
  <span class="c"># crt files</span>
  <span class="nv">crtFiles</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">concatStringsSep</span> <span class="s2">" "</span> <span class="p">(</span><span class="kr">map</span> <span class="p">(</span><span class="nv">d</span><span class="p">:</span>
    <span class="s2">"crt /var/lib/haproxy/certs/</span><span class="si">${</span><span class="nv">d</span><span class="si">}</span><span class="s2">.pem"</span>
  <span class="p">)</span> <span class="nv">serverNames</span><span class="p">);</span>
  <span class="c"># backend definitions</span>
  <span class="nv">backendDefs</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">concatStringsSep</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span> <span class="p">(</span><span class="kr">map</span> <span class="p">(</span><span class="nv">d</span><span class="p">:</span>
    <span class="kd">let</span>
      <span class="nv">backendName</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">replaceStrings</span> <span class="p">[</span> <span class="s2">"."</span> <span class="p">]</span> <span class="p">[</span> <span class="s2">"_"</span> <span class="p">]</span> <span class="nv">d</span><span class="p">;</span>
      <span class="nv">backendAddr</span> <span class="o">=</span> <span class="nv">serverBackends</span><span class="o">.</span><span class="p">${</span><span class="nv">d</span><span class="p">};</span>
      <span class="nv">indent</span> <span class="o">=</span> <span class="s2">"    "</span><span class="p">;</span> <span class="c"># 1 tab = 4 spaces</span>
    <span class="kn">in</span> <span class="s2">''</span><span class="err">
</span><span class="s2">      backend </span><span class="si">${</span><span class="nv">backendName</span><span class="si">}</span><span class="s2">_app</span><span class="err">
</span><span class="s2">        mode http</span><span class="err">
</span><span class="s2">        server </span><span class="si">${</span><span class="nv">backendName</span><span class="si">}</span><span class="s2">_1 </span><span class="si">${</span><span class="nv">backendAddr</span><span class="si">}</span><span class="s2"> check inter 2s fall 3 rise 2</span><span class="err">
</span><span class="s2">        errorfiles customerrors</span><span class="err">
</span><span class="s2">    ''</span>
  <span class="p">)</span> <span class="nv">serverNames</span><span class="p">);</span>
<span class="kn">in</span> <span class="p">{</span>
  <span class="c"># SSL certificates for HAProxy</span>
  <span class="nv">security</span><span class="o">.</span><span class="nv">acme</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">acceptTerms</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">defaults</span><span class="o">.</span><span class="nv">email</span> <span class="o">=</span> <span class="nv">acme_email</span><span class="p">;</span>
    <span class="nv">certs</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">listToAttrs</span> <span class="p">(</span><span class="kr">map</span> <span class="p">(</span><span class="nv">domain</span><span class="p">:</span> <span class="p">{</span>
      <span class="nv">name</span> <span class="o">=</span> <span class="nv">domain</span><span class="p">;</span>
      <span class="nv">value</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nv">webroot</span> <span class="o">=</span> <span class="s2">"/var/lib/acme/acme-challenge"</span><span class="p">;</span>
        <span class="nv">group</span> <span class="o">=</span> <span class="s2">"haproxy"</span><span class="p">;</span>
        <span class="nv">postRun</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">          mkdir -p /var/lib/haproxy/certs</span><span class="err">
</span><span class="s2">          cat /var/lib/acme/</span><span class="si">${</span><span class="nv">domain</span><span class="si">}</span><span class="s2">/fullchain.pem \</span><span class="err">
</span><span class="s2">            /var/lib/acme/</span><span class="si">${</span><span class="nv">domain</span><span class="si">}</span><span class="s2">/key.pem \</span><span class="err">
</span><span class="s2">            &gt; /var/lib/haproxy/certs/</span><span class="si">${</span><span class="nv">domain</span><span class="si">}</span><span class="s2">.pem</span><span class="err">
</span><span class="s2">          chown root:haproxy /var/lib/haproxy/certs/</span><span class="si">${</span><span class="nv">domain</span><span class="si">}</span><span class="s2">.pem</span><span class="err">
</span><span class="s2">          chmod 0640 /var/lib/haproxy/certs/</span><span class="si">${</span><span class="nv">domain</span><span class="si">}</span><span class="s2">.pem</span><span class="err">
</span><span class="s2">        ''</span><span class="p">;</span>
      <span class="p">};</span>
    <span class="p">})</span> <span class="nv">serverNames</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="c"># HAProxy service</span>
  <span class="nv">services</span><span class="o">.</span><span class="nv">haproxy</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">config</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">      global</span><span class="err">
</span><span class="s2">        log stdout format raw local0</span><span class="err">
</span><span class="s2">        maxconn 2000</span><span class="err">

</span><span class="s2">      defaults</span><span class="err">
</span><span class="s2">        log global</span><span class="err">
</span><span class="s2">        mode http</span><span class="err">
</span><span class="s2">        option httplog</span><span class="err">
</span><span class="s2">        option dontlognull</span><span class="err">
</span><span class="s2">        option forwardfor except 127.0.0.1</span><span class="err">
</span><span class="s2">        timeout connect 5s</span><span class="err">
</span><span class="s2">        timeout client 30s</span><span class="err">
</span><span class="s2">        timeout server 30s</span><span class="err">
</span><span class="s2">        retries 3</span><span class="err">

</span><span class="s2">      frontend http_in</span><span class="err">
</span><span class="s2">        bind *:80</span><span class="err">
</span><span class="s2">        acl acme_challenge path_beg /.well-known/acme-challenge/</span><span class="err">
</span><span class="s2">        use_backend acme_backend if acme_challenge</span><span class="err">
</span><span class="s2">        acl host_zt hdr(host) -i stephen.zerotier</span><span class="err">
</span><span class="s2">        </span><span class="si">${</span><span class="nv">aclLines</span><span class="si">}</span><span class="err">
</span><span class="s2">        use_backend something_one_maikeladas_es_app if host_zt</span><span class="err">
</span><span class="s2">        </span><span class="si">${</span><span class="nv">redirectLines</span><span class="si">}</span><span class="err">
</span><span class="s2">        default_backend not_found</span><span class="err">

</span><span class="s2">      frontend https_in</span><span class="err">
</span><span class="s2">        bind *:443 ssl </span><span class="si">${</span><span class="nv">crtFiles</span><span class="si">}</span><span class="err">
</span><span class="s2">        mode http</span><span class="err">
</span><span class="s2">        acl acme_challenge path_beg /.well-known/acme-challenge/</span><span class="err">
</span><span class="s2">        </span><span class="si">${</span><span class="nv">aclLines</span><span class="si">}</span><span class="err">
</span><span class="s2">        use_backend acme_backend if acme_challenge</span><span class="err">
</span><span class="s2">        </span><span class="si">${</span><span class="nv">backendLines</span><span class="si">}</span><span class="err">
</span><span class="s2">        default_backend not_found</span><span class="err">

</span><span class="s2">      backend acme_backend</span><span class="err">
</span><span class="s2">        mode http</span><span class="err">
</span><span class="s2">        server local_acme 127.0.0.1:8081</span><span class="err">

</span><span class="s2">      </span><span class="si">${</span><span class="nv">backendDefs</span><span class="si">}</span><span class="err">

</span><span class="s2">      http-errors customerrors</span><span class="err">
</span><span class="s2">        errorfile 503 /etc/haproxy/errorfiles/503.http</span><span class="err">

</span><span class="s2">      backend not_found</span><span class="err">
</span><span class="s2">        mode http</span><span class="err">
</span><span class="s2">        errorfiles customerrors</span><span class="err">
</span><span class="s2">    ''</span><span class="p">;</span>
    <span class="nv">user</span> <span class="o">=</span> <span class="s2">"haproxy"</span><span class="p">;</span>
    <span class="nv">group</span> <span class="o">=</span> <span class="s2">"haproxy"</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="nv">environment</span><span class="o">.</span><span class="nv">etc</span><span class="o">.</span><span class="s2">"haproxy/errorfiles/503.http"</span><span class="o">.</span><span class="nv">text</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    HTTP/1.1 503 Service Unavailable</span><span class="err">
</span><span class="s2">    Cache-Control: no-cache</span><span class="err">
</span><span class="s2">    Connection: close</span><span class="err">
</span><span class="s2">    Content-Type: text/html</span><span class="err">
</span><span class="s2">    </span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">readFile</span> <span class="sx">./haproxy-error-503.html</span><span class="si">}</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>

  <span class="c"># Local HTTP server to serve ACME challenges</span>
  <span class="nv">systemd</span><span class="o">.</span><span class="nv">services</span><span class="o">.</span><span class="nv">acme-challenge-server</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">description</span> <span class="o">=</span> <span class="s2">"Serve Let's Encrypt challenges for HAProxy"</span><span class="p">;</span>
    <span class="nv">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"multi-user.target"</span> <span class="p">];</span>
    <span class="nv">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">ExecStart</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">pkgs</span><span class="o">.</span><span class="nv">python3</span><span class="si">}</span><span class="s2">/bin/python3 -m http.server 8081 --directory /var/lib/acme/acme-challenge"</span><span class="p">;</span>
      <span class="nv">Restart</span> <span class="o">=</span> <span class="s2">"always"</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="your-laptop-or-other-pc-configuration-to-be-accessible">Your laptop or other PC configuration to be accessible</h1>

<p>Assuming you use Zerotier (is free) and have it in the same network as the server (I do) and you know the IP or domain name if you have it in /etc/hosts (I do but I prefer to use IPs). Notice how I’ve limited the listening interfaces to that one of Zerotier only.</p>

<p>Of course import this file in your configuration for that machine. I call it <code class="language-plaintext highlighter-rouge">haproxy-redirect.nix</code> to make it clear what the nix contains.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">config</span><span class="p">,</span> <span class="nv">pkgs</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="p">{</span>
  <span class="c"># HAProxy service</span>
  <span class="nv">services</span><span class="o">.</span><span class="nv">haproxy</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">config</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">      global</span><span class="err">
</span><span class="s2">        log stdout format raw local0</span><span class="err">
</span><span class="s2">        maxconn 2000</span><span class="err">

</span><span class="s2">      defaults</span><span class="err">
</span><span class="s2">        log global</span><span class="err">
</span><span class="s2">        mode http</span><span class="err">
</span><span class="s2">        option httplog</span><span class="err">
</span><span class="s2">        option dontlognull</span><span class="err">
</span><span class="s2">        option forwardfor except 127.0.0.1</span><span class="err">
</span><span class="s2">        timeout connect 5s</span><span class="err">
</span><span class="s2">        timeout client 30s</span><span class="err">
</span><span class="s2">        timeout server 30s</span><span class="err">
</span><span class="s2">        retries 3</span><span class="err">

</span><span class="s2">      frontend local_http</span><span class="err">
</span><span class="s2">        bind thinkpad.zerotier:8080</span><span class="err">
</span><span class="s2">        default_backend phoenix_app</span><span class="err">

</span><span class="s2">      backend phoenix_app</span><span class="err">
</span><span class="s2">        server phoenix_1 127.0.0.1:4000 check inter 2s fall 3 rise 2</span><span class="err">
</span><span class="s2">        errorfile 503 /etc/haproxy/errorfiles/503.http</span><span class="err">
</span><span class="s2">    ''</span><span class="p">;</span>
    <span class="nv">user</span> <span class="o">=</span> <span class="s2">"haproxy"</span><span class="p">;</span>
    <span class="nv">group</span> <span class="o">=</span> <span class="s2">"haproxy"</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="nv">environment</span><span class="o">.</span><span class="nv">etc</span><span class="o">.</span><span class="s2">"haproxy/errorfiles/503.http"</span><span class="o">.</span><span class="nv">text</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    HTTP/1.1 503 Service Unavailable</span><span class="err">
</span><span class="s2">    Cache-Control: no-cache</span><span class="err">
</span><span class="s2">    Connection: close</span><span class="err">
</span><span class="s2">    Content-Type: text/html</span><span class="err">
</span><span class="s2">    </span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">readFile</span> <span class="sx">./haproxy-error-503.html</span><span class="si">}</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-default-503-error-file">The default 503 error file</h2>

<p><img src="/assets/images/2025-10-image-1.png" alt="Image" /></p>

<p>All errors I got when I turned off any of them servers were of type 503 so that’s the error I customised. The files is <code class="language-plaintext highlighter-rouge">haproxy-error-503.html</code> and should be in the same folder as the nix configuration. The content is this</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;title&gt;</span>Service Unavailable<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;style&gt;</span>
      <span class="nt">body</span> <span class="p">{</span>
        <span class="nl">font-family</span><span class="p">:</span> <span class="o">-</span><span class="n">apple-system</span><span class="p">,</span> <span class="n">BlinkMacSystemFont</span><span class="p">,</span> <span class="s1">"Segoe UI"</span><span class="p">,</span> <span class="n">Roboto</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span>
        <span class="nl">background</span><span class="p">:</span> <span class="nx">#fafafa</span><span class="p">;</span>
        <span class="nl">color</span><span class="p">:</span> <span class="nx">#333</span><span class="p">;</span>
        <span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
        <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
        <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
        <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
        <span class="nl">height</span><span class="p">:</span> <span class="m">100vh</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="nt">main</span> <span class="p">{</span>
        <span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
        <span class="nl">padding</span><span class="p">:</span> <span class="m">2rem</span><span class="p">;</span>
        <span class="nl">background</span><span class="p">:</span> <span class="nx">white</span><span class="p">;</span>
        <span class="nl">border-radius</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
        <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">4px</span> <span class="m">12px</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.08</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="nt">h1</span> <span class="p">{</span>
        <span class="nl">font-size</span><span class="p">:</span> <span class="m">2rem</span><span class="p">;</span>
        <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>
        <span class="nl">color</span><span class="p">:</span> <span class="nx">#c0392b</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="nt">p</span> <span class="p">{</span>
        <span class="nl">font-size</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
        <span class="nl">color</span><span class="p">:</span> <span class="nx">#666</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="nt">&lt;/style&gt;</span>
  <span class="nt">&lt;/head&gt;</span>
  <span class="nt">&lt;body&gt;</span>
    <span class="nt">&lt;main&gt;</span>
      <span class="nt">&lt;h1&gt;</span>503 – Service Unavailable<span class="nt">&lt;/h1&gt;</span>
      <span class="nt">&lt;p&gt;</span>Our server is taking a quick break. Please try again in a moment.<span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;/main&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<h1 id="advantages-over-the-pagekite-way">Advantages over the Pagekite way</h1>

<ol>
  <li>HAProxy is a lot more robust.</li>
  <li>Since you’re using already a machine as server and Zerotier for LAN-like communication, this harness precisely all that to make the config a lot simpler.</li>
  <li>SSL renewal bult-in.</li>
  <li>You can just stop the services whenever, the 503 custom-error page will be shown. No need to remember to “sysctl stop whatever”</li>
  <li>Custom error page.</li>
</ol>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="NixOS" /><category term="DevOps" /><category term="networking" /><category term="tunneling" /><category term="SSL" /><summary type="html"><![CDATA[Another way to do the same tunneling without Ngrok or any paid service, but this time with autorenewal of the SSL and in a more robust and simple manner.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1637481687365.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1637481687365.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pagekite with Custom Domain in Nixos, tunneling without Ngrok for free</title><link href="https://blog.maikel.dev/2025/10/09/pagekite-with-custom-domain-in-nixos-tunneling-without-ngrok-for-free.html" rel="alternate" type="text/html" title="Pagekite with Custom Domain in Nixos, tunneling without Ngrok for free" /><published>2025-10-09T00:00:00+00:00</published><updated>2025-10-09T00:00:00+00:00</updated><id>https://blog.maikel.dev/2025/10/09/pagekite-with-custom-domain-in-nixos-tunneling-without-ngrok-for-free</id><content type="html" xml:base="https://blog.maikel.dev/2025/10/09/pagekite-with-custom-domain-in-nixos-tunneling-without-ngrok-for-free.html"><![CDATA[<p>There are instructions all over on how to configure Pagekite on Ubuntu but none on NixOS there’s not even a derivation of it so I made one. I’m tired of Zrok, Ngrok, Tailscale and all of their offerings that are either closed-source or impossibly hard to install or maintain. Let alone **to use a custom domain **you’ll have to pay north of 20 USD per month.</p>

<p>I just want to be able to use my own domains, that’s all. I don’t need any of the other fancy stuff so even though Pagekite is a lot cheaper than all of them 3, to use custom domains **with **SSL is not so much. So I decided to self-host the frontend. Btw, whoever decided to call the server “frontend” and the client “backend” deserves to be hanged upside down in a bucket filled with piranhas.</p>

<h2 id="getting-ssl">Getting SSL</h2>

<p>Think that here <code class="language-plaintext highlighter-rouge">DOMAIN</code> is going to be the parent domain. I could make it be directly maikeladas.es but I have other stuff there and using a wildcard would force me to get rid of subdomains so I rather use a subdomain for specific temporary stuff, in this case <code class="language-plaintext highlighter-rouge">dev.maikeladas.es</code>. I could have used <code class="language-plaintext highlighter-rouge">maikel.dev</code>but the restrictions of dev domains that you cannot use port 80. They force you to use 443 and SSL. I want flexibility.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nv">$HOME</span>/certs
<span class="nb">set </span>DOMAIN dev.maikeladas.es
nix-shell <span class="nt">-p</span> certbot
certbot certonly <span class="nt">--manual</span> <span class="se">\</span>
  <span class="nt">--work-dir</span><span class="o">=</span><span class="nv">$HOME</span>/certs <span class="nt">--logs-dir</span><span class="o">=</span>/tmp/ <span class="nt">--config-dir</span><span class="o">=</span><span class="nv">$HOME</span>/certs <span class="se">\</span>
  <span class="nt">--preferred-challenges</span><span class="o">=</span>dns <span class="se">\</span>
  <span class="nt">--email</span> avalid@email.whatever <span class="se">\</span>
  <span class="nt">--server</span> https://acme-v02.api.letsencrypt.org/directory <span class="se">\</span>
  <span class="nt">--agree-tos</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"*.</span><span class="nv">$DOMAIN</span><span class="s2">"</span>
<span class="nb">exit
sudo cat</span> <span class="se">\</span>
  <span class="nv">$HOME</span>/certs/live/<span class="nv">$DOMAIN</span>/fullchain.pem <span class="se">\</span>
  <span class="nv">$HOME</span>/certs/live/<span class="nv">$DOMAIN</span>/privkey.pem <span class="se">\</span>
  | <span class="nb">sudo tee</span> <span class="nv">$HOME</span>/certs/keycert.pem <span class="o">&gt;</span> /dev/null
</code></pre></div></div>

<p>Now you have the keycert.pem file that Pagekite requires to provide domains over HTTPS. The certificate will require renewal every 6 months. It can be automated but that’s for another day.</p>

<h2 id="do-your-dns-changes">Do your DNS changes</h2>

<p>Go to your DNS provider. For the domain create a <code class="language-plaintext highlighter-rouge">*.dev.YOURDOMAIN.COM</code> record of A type pointing to the IP and one of the parent subdomain pointing to the same IP. So also <code class="language-plaintext highlighter-rouge">dev.YOURDOMAIN.COM</code> again of A type.</p>

<h2 id="nixos-changes-to-get-pagekite">NixOS Changes to get Pagekite</h2>

<p>Assuming you use configuration.nix or somefile.nix that is not a flake.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">config</span><span class="p">,</span> <span class="nv">lib</span><span class="p">,</span> <span class="nv">pkgs</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="kd">let</span>
  <span class="nv">kitesecret</span> <span class="o">=</span> <span class="s2">"YOUR_KITE_PASS"</span><span class="p">;</span>
  <span class="nv">pagekite</span> <span class="o">=</span> <span class="kr">import</span> <span class="sx">./pagekite-package.nix</span> <span class="p">{</span> <span class="kn">inherit</span> <span class="nv">pkgs</span><span class="p">;</span> <span class="p">};</span>
<span class="kn">in</span> <span class="p">{</span>
  <span class="c"># The rest of your config</span>
  <span class="c"># To install the derivation while keeping it around to pass it</span>
  <span class="nv">environment</span><span class="o">.</span><span class="nv">systemPackages</span> <span class="o">=</span> <span class="nv">lib</span><span class="o">.</span><span class="nv">mkAfter</span> <span class="p">[</span> <span class="nv">pagekite</span> <span class="p">];</span>
  <span class="nv">imports</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c"># ...your other imports</span>
    <span class="c"># Import as functions, passing pagekite and kitesecret explicitly</span>
    <span class="c"># If this machine is the client</span>
    <span class="p">(</span><span class="kr">import</span> <span class="sx">./pagekite-client.nix</span> <span class="p">{</span>
      <span class="kn">inherit</span> <span class="nv">config</span> <span class="nv">pkgs</span> <span class="nv">lib</span> <span class="nv">pagekite</span> <span class="nv">kitesecret</span><span class="p">;</span>
      <span class="nv">frontendHost</span> <span class="o">=</span> <span class="s2">"dev.maikeladas.es"</span><span class="p">;</span>
      <span class="nv">frontendPort</span> <span class="o">=</span> <span class="mi">80</span><span class="p">;</span>
      <span class="nv">backendHost</span> <span class="o">=</span> <span class="s2">"stephen.dev.maikeladas.es"</span><span class="p">;</span>
      <span class="nv">backendLocalPort</span> <span class="o">=</span> <span class="mi">4000</span><span class="p">;</span>
    <span class="p">})</span>
    <span class="c"># If this machine is the server</span>
    <span class="p">(</span><span class="kr">import</span> <span class="sx">./pagekite-server.nix</span> <span class="p">{</span>
      <span class="kn">inherit</span> <span class="nv">config</span> <span class="nv">pkgs</span> <span class="nv">lib</span> <span class="nv">pagekite</span> <span class="nv">kitesecret</span><span class="p">;</span>
      <span class="nv">kitename</span> <span class="o">=</span> <span class="s2">"*.dev.maikeladas.es"</span><span class="p">;</span>
      <span class="nv">ports</span> <span class="o">=</span> <span class="s2">"80,443"</span><span class="p">;</span>
      <span class="nv">protos</span> <span class="o">=</span> <span class="s2">"http,https"</span><span class="p">;</span>
      <span class="nv">domainHttp</span> <span class="o">=</span> <span class="s2">"*.dev.maikeladas.es"</span><span class="p">;</span>
      <span class="nv">domainHttps</span> <span class="o">=</span> <span class="s2">"*.dev.maikeladas.es"</span><span class="p">;</span>
      <span class="nv">tlsEndpoint</span> <span class="o">=</span> <span class="s2">"*.dev.maikeladas.es:/home/maikel/certs/keycert.pem"</span><span class="p">;</span>
    <span class="p">})</span>
  <span class="p">];</span>
  <span class="c"># The rest of your /etc/nixos/configuration.nix file</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="then-the-pagekite-packagenix">Then the pagekite-package.nix</h2>

<p>This is all you need to intsall pagekite.py</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">pkgs</span> <span class="p">}:</span>
<span class="nv">pkgs</span><span class="o">.</span><span class="nv">stdenv</span><span class="o">.</span><span class="nv">mkDerivation</span> <span class="kr">rec</span> <span class="p">{</span>
  <span class="nv">pname</span> <span class="o">=</span> <span class="s2">"pagekite"</span><span class="p">;</span>
  <span class="nv">version</span> <span class="o">=</span> <span class="s2">"1.0"</span><span class="p">;</span>
  <span class="nv">src</span> <span class="o">=</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">fetchurl</span> <span class="p">{</span>
    <span class="nv">url</span> <span class="o">=</span> <span class="s2">"https://pagekite.net/pk/pagekite.py"</span><span class="p">;</span>
    <span class="nv">sha256</span> <span class="o">=</span> <span class="s2">"1nqa4nkhjq2shc7zpxn22pxfqpsl6xf06mfxlwa72c5p72zf7x94"</span><span class="p">;</span>
  <span class="p">};</span>
  <span class="nv">nativeBuildInputs</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">makeWrapper</span> <span class="p">];</span>
  <span class="nv">unpackPhase</span> <span class="o">=</span> <span class="s2">":"</span><span class="p">;</span>
  <span class="nv">installPhase</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    mkdir -p $out/bin</span><span class="err">
</span><span class="s2">    sed "1 s|^.*$|#!</span><span class="si">${</span><span class="nv">pkgs</span><span class="o">.</span><span class="nv">python3</span><span class="si">}</span><span class="s2">/bin/python3|" $src &gt; $out/bin/pagekite</span><span class="err">
</span><span class="s2">    chmod +x $out/bin/pagekite</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="then-the-pagekite-clientnix">Then the pagekite-client.nix</h2>

<p>This is the one to use a client.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">config</span><span class="p">,</span> <span class="nv">pkgs</span><span class="p">,</span> <span class="nv">lib</span><span class="p">,</span> <span class="nv">kitesecret</span><span class="p">,</span> <span class="nv">pagekite</span><span class="p">,</span> <span class="nv">frontendHost</span><span class="p">,</span> <span class="nv">frontendPort</span><span class="p">,</span> <span class="nv">backendHost</span><span class="p">,</span> <span class="nv">backendLocalPort</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="kd">let</span>
  <span class="c"># Construct addresses</span>
  <span class="nv">frontend</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">frontendHost</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="kr">toString</span> <span class="nv">frontendPort</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
  <span class="nv">backend</span> <span class="o">=</span> <span class="s2">"http:</span><span class="si">${</span><span class="nv">backendHost</span><span class="si">}</span><span class="s2">:localhost:</span><span class="si">${</span><span class="kr">toString</span> <span class="nv">backendLocalPort</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
<span class="kn">in</span> <span class="p">{</span>
  <span class="c"># PageKite client configuration file</span>
  <span class="nv">environment</span><span class="o">.</span><span class="nv">etc</span><span class="o">.</span><span class="s2">"pagekite.d/</span><span class="si">${</span><span class="nv">backendHost</span><span class="si">}</span><span class="s2">.conf"</span><span class="o">.</span><span class="nv">text</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    frontend = </span><span class="si">${</span><span class="nv">frontend</span><span class="si">}</span><span class="err">
</span><span class="s2">    service_on = </span><span class="si">${</span><span class="nv">backend</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nv">kitesecret</span><span class="si">}</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>

  <span class="c"># Systemd service</span>
  <span class="nv">systemd</span><span class="o">.</span><span class="nv">services</span><span class="o">.</span><span class="s2">"pagekite-client-</span><span class="si">${</span><span class="nv">backendHost</span><span class="si">}</span><span class="s2">"</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">description</span> <span class="o">=</span> <span class="s2">"PageKite Client Tunnel Service for </span><span class="si">${</span><span class="nv">backendHost</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="nv">after</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"network.target"</span> <span class="p">];</span>
    <span class="nv">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"multi-user.target"</span> <span class="p">];</span>
    <span class="nv">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">Type</span> <span class="o">=</span> <span class="s2">"simple"</span><span class="p">;</span>
      <span class="nv">ExecStart</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">pagekite</span><span class="si">}</span><span class="s2">/bin/pagekite --optfile /etc/pagekite.d/</span><span class="si">${</span><span class="nv">backendHost</span><span class="si">}</span><span class="s2">.conf"</span><span class="p">;</span>
      <span class="nv">Restart</span> <span class="o">=</span> <span class="s2">"always"</span><span class="p">;</span>
      <span class="nv">RestartSec</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
      <span class="nv">User</span> <span class="o">=</span> <span class="s2">"root"</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="then-the-pagekite-servernix">Then the pagekite-server.nix</h2>

<p>This is the one to run a server. This machine needs to be one that responds when you ping <code class="language-plaintext highlighter-rouge">dev.YOURDOMAIN.COM</code> and <code class="language-plaintext highlighter-rouge">*.dev.YOURDOMAIN.COM</code> so you must have your DNS configured correctly.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">config</span><span class="p">,</span> <span class="nv">pkgs</span><span class="p">,</span> <span class="nv">lib</span><span class="p">,</span> <span class="nv">kitesecret</span><span class="p">,</span> <span class="nv">pagekite</span><span class="p">,</span> <span class="nv">kitename</span><span class="p">,</span> <span class="nv">ports</span><span class="p">,</span> <span class="nv">protos</span><span class="p">,</span> <span class="nv">domainHttp</span><span class="p">,</span> <span class="nv">domainHttps</span><span class="p">,</span> <span class="nv">tlsEndpoint</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span>
<span class="p">{</span>
  <span class="c"># PageKite server configuration file</span>
  <span class="nv">environment</span><span class="o">.</span><span class="nv">etc</span><span class="o">.</span><span class="s2">"pagekite.d/pk-server.conf"</span><span class="o">.</span><span class="nv">text</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    kitename = </span><span class="si">${</span><span class="nv">kitename</span><span class="si">}</span><span class="err">
</span><span class="s2">    kitesecret = </span><span class="si">${</span><span class="nv">kitesecret</span><span class="si">}</span><span class="err">
</span><span class="s2">    isfrontend</span><span class="err">
</span><span class="s2">    ports = </span><span class="si">${</span><span class="nv">ports</span><span class="si">}</span><span class="err">
</span><span class="s2">    protos = </span><span class="si">${</span><span class="nv">protos</span><span class="si">}</span><span class="err">
</span><span class="s2">    domain = http:</span><span class="si">${</span><span class="nv">domainHttp</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nv">kitesecret</span><span class="si">}</span><span class="err">
</span><span class="s2">    domain = https:</span><span class="si">${</span><span class="nv">domainHttps</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nv">kitesecret</span><span class="si">}</span><span class="err">
</span><span class="s2">    tls_endpoint = </span><span class="si">${</span><span class="nv">tlsEndpoint</span><span class="si">}</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>

  <span class="c"># Systemd service</span>
  <span class="nv">systemd</span><span class="o">.</span><span class="nv">services</span><span class="o">.</span><span class="nv">pagekite</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">description</span> <span class="o">=</span> <span class="s2">"PageKite Reverse Tunnel Service"</span><span class="p">;</span>
    <span class="nv">after</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"network.target"</span> <span class="p">];</span>
    <span class="nv">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"multi-user.target"</span> <span class="p">];</span>
    <span class="nv">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">Type</span> <span class="o">=</span> <span class="s2">"simple"</span><span class="p">;</span>
      <span class="nv">ExecStart</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">pagekite</span><span class="si">}</span><span class="s2">/bin/pagekite --optfile /etc/pagekite.d/pk-server.conf"</span><span class="p">;</span>
      <span class="nv">Restart</span> <span class="o">=</span> <span class="s2">"always"</span><span class="p">;</span>
      <span class="nv">RestartSec</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
      <span class="nv">User</span> <span class="o">=</span> <span class="s2">"root"</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="how-to-use-it">How to use it</h1>

<p>On whatever machine plays as server add the necessary files to run the server, and on the client for the client. Both cases need pagekite-package.nix though. Then just <code class="language-plaintext highlighter-rouge">nixos-rebuild switch</code> or whatever NixOS method you use to update your config.</p>

<p>I normally do a <code class="language-plaintext highlighter-rouge">sudo systemctl stop pagekite-client-DOMAIN</code> after running any client the first time so it’s only available when I need it.
<img src="/assets/images/2025-10-image.png" alt="Image" /></p>
<h2 id="caveats">Caveats</h2>

<ol>
  <li>I haven’t automated SSL renewal, it can be done, but I need to have a life. I might not be using this in six months.</li>
  <li>If you stop whatever is you’re serving or the server, the client hangs and does not auto-reconnect. So get ready to restart the client.</li>
  <li>You won’t have to pay ever again Zrok, Ngrok or Tailscale for using a custom domain. Oh, the suffering!</li>
</ol>

<!--kg-card-begin: html-->
<div><iframe src="https://giphy.com/embed/5WXqTFTgO9a7e" frameborder="0" allowfullscreen=""></iframe></div>
<!--kg-card-end: html-->
<p>If you liked this article consider tipping me on Ko-Fi 👇
<!--kg-card-begin: html-->
<script type="text/javascript" src="https://storage.ko-fi.com/cdn/widget/Widget_2.js"></script><script type="text/javascript">kofiwidget2.init('Support me on Ko-fi', '#72a4f2', 'H2H4GCC0N');kofiwidget2.draw();</script>
<!--kg-card-end: html--></p>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="NixOS" /><category term="DevOps" /><category term="networking" /><category term="tunneling" /><summary type="html"><![CDATA[I got tired of Ngrok, Zrok, Tailscale and all of these options that charge you absurds amounts of money for just letting you use your own domain names in a web tunnel to try APIs or development. So I made my own derivation in NixOS to run Pagekite.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1610098905401.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1610098905401.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Running FreeBSD from NixOS using Libvirtd from scratch</title><link href="https://blog.maikel.dev/2025/09/27/running-freebsd-from-nixos-using-libvirtd-from-scratch.html" rel="alternate" type="text/html" title="Running FreeBSD from NixOS using Libvirtd from scratch" /><published>2025-09-27T00:00:00+00:00</published><updated>2025-09-27T00:00:00+00:00</updated><id>https://blog.maikel.dev/2025/09/27/running-freebsd-from-nixos-using-libvirtd-from-scratch</id><content type="html" xml:base="https://blog.maikel.dev/2025/09/27/running-freebsd-from-nixos-using-libvirtd-from-scratch.html"><![CDATA[<p>I use <strong>FreeBSD</strong> for work because my clients deploy servers on it. At home, I have a PC with 32 GB of RAM and use <strong>NixOS</strong>, so I wanted to run FreeBSD locally for quick tests.</p>

<p>My first choice would normally be <strong>VirtualBox</strong>, but on NixOS it’s a pain: every system upgrade forces VirtualBox to be recompiled. Since I upgrade often, that became unmanageable.</p>

<p>People in the fediverse suggested <strong>libvirtd</strong>, so I gave it a try. It’s trickier at first, but once you learn a few commands it’s not bad at all—and in fact, it allows for a lot of automation.</p>

<details>
  <summary>Version History</summary>

  <p>This guide has evolved through several iterations:</p>

  <p><strong>v3.0 (Current)</strong></p>
  <ul>
    <li>Removed <code class="language-plaintext highlighter-rouge">add_vm_to_network</code> Fish-shell function (no longer necessary with NSS)</li>
    <li>Fixed <code class="language-plaintext highlighter-rouge">create_vm</code> Fish-shell function (no longer needs MACs)</li>
    <li>Simplified network edition</li>
    <li>Added NSS configuration to <code class="language-plaintext highlighter-rouge">configuration.nix</code> for hostname resolution</li>
    <li>Set default URI to avoid adding <code class="language-plaintext highlighter-rouge">-c qemu:///system</code> to every command</li>
    <li>Improved <code class="language-plaintext highlighter-rouge">clone_user_data</code> function with automatic folder creation</li>
    <li>Added repo with all functions</li>
  </ul>

  <p><strong>v2.0</strong></p>
  <ul>
    <li>Removed UFS images to avoid duplication (ZFS only)</li>
    <li>Added Fish-shell scripts to automate creation, destruction and network config</li>
    <li>Fixed IP assignment to VMs using MACs and hostnames</li>
    <li>Added KDE desktop environment setup via cloud-init</li>
    <li>Added Zerotier integration with self-authorization</li>
  </ul>

  <p><strong>v1.0</strong></p>
  <ul>
    <li>Initial guide with basic libvirtd setup</li>
    <li>UFS and ZFS image support</li>
    <li>Basic cloud-init configuration</li>
  </ul>

</details>

<h1 id="installing-libvirtd">Installing Libvirtd</h1>

<p>In <code class="language-plaintext highlighter-rouge">configuration.nix</code> you need to make the following changes.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Add the user to these groups</span>
<span class="nv">users</span><span class="o">.</span><span class="nv">users</span><span class="o">.</span><span class="nv">maikel</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">extraGroups</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"libvirtd"</span><span class="p">,</span> <span class="s2">"libvirt"</span><span class="p">,</span> <span class="s2">"qemu-libvirtd"</span> <span class="p">];</span>
<span class="p">};</span>

<span class="c"># Enable libvirtd</span>
<span class="nv">virtualisation</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">libvirtd</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">qemu</span><span class="o">.</span><span class="nv">vhostUserPackages</span> <span class="o">=</span> <span class="kn">with</span> <span class="nv">pkgs</span><span class="p">;</span> <span class="p">[</span> <span class="nv">virtiofsd</span> <span class="p">];</span>
    <span class="c"># Enable NSS plugin for resolving VM hostnames</span>
    <span class="nv">nss</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>        <span class="c"># classic libvirt NSS</span>
      <span class="nv">enableGuest</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>    <span class="c"># resolves domain names of guests</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">};</span>

<span class="c"># Add Virt-Manager makes simpler to explore actual configs</span>
<span class="nv">programs</span><span class="o">.</span><span class="nv">virt-manager</span><span class="o">.</span><span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

<span class="nv">environment</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">systemPackages</span> <span class="o">=</span> <span class="kn">with</span> <span class="nv">pkgs</span><span class="p">;</span> <span class="p">[</span>
    <span class="nv">virt-viewer</span>
    <span class="nv">cloud-utils</span>
    <span class="nv">cloud-init</span>
  <span class="p">];</span>
<span class="p">};</span>
</code></pre></div></div>

<h1 id="running-commands-when-using-libvirtd-as-a-systems-service">Running commands when using Libvirtd as a system’s service</h1>

<p>Despite all the changes there, I still need to prepend all virsh and virt-viewer commands with <code class="language-plaintext highlighter-rouge">-c qemu:///system</code>. One way to avoid having to do this is to set the environment variable.</p>

<p>For Fish shell:</p>

<pre><code class="language-fish"># Run this from anywhere, it automatically stores it in ~/.config/fish/fish_variables
set -Ux LIBVIRT_DEFAULT_URI qemu:///system
</code></pre>

<p>For Bash:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Append to ~/.bashrc or ~/.profile, whichever is the last to load on your system</span>
<span class="nb">export </span><span class="nv">LIBVIRT_DEFAULT_URI</span><span class="o">=</span><span class="s2">"qemu:///system"</span>
</code></pre></div></div>

<p>Alternatively, you can use Fish abbreviations (which I prefer because they show you the full command):</p>

<pre><code class="language-fish"># Add to ~/.config/fish/conf.d/abbreviations.fish
abbr --add virt-viewer "virt-viewer -c qemu:///system"
abbr --add virsh "virsh -c qemu:///system"
</code></pre>

<p>I like <code class="language-plaintext highlighter-rouge">abbr</code> instead of <code class="language-plaintext highlighter-rouge">alias</code> because with <code class="language-plaintext highlighter-rouge">abbr</code> <strong>I don’t actually forget</strong> the full command ever. It’s shown to me every time.</p>

<h1 id="configuring-the-network">Configuring the Network</h1>

<p>By default there’s nothing running network wise. So you need to start the default network with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Starts it if you can't see any new networks in ifconfig</span>
virsh net-start default

<span class="c"># So it autostarts</span>
virsh net-autostart default
</code></pre></div></div>

<p>That will run the virtual network every time the PC reboots.</p>

<h2 id="modifying-the-default-network-to-your-desired-ip-range">Modifying the default network to your desired IP range</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>virsh net-dumpxml default <span class="o">&gt;</span> mynetwork.xml
</code></pre></div></div>

<p>We get this from it on the file <code class="language-plaintext highlighter-rouge">mynetwork.xml</code>:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;network&gt;</span>
  <span class="nt">&lt;name&gt;</span>default<span class="nt">&lt;/name&gt;</span>
  <span class="nt">&lt;uuid&gt;</span>07c8b831-3fa7-4bb1-ae07-fad64b672a67<span class="nt">&lt;/uuid&gt;</span>
  <span class="nt">&lt;forward</span> <span class="na">mode=</span><span class="s">'nat'</span><span class="nt">&gt;</span>
    <span class="nt">&lt;nat&gt;</span>
      <span class="nt">&lt;port</span> <span class="na">start=</span><span class="s">'1024'</span> <span class="na">end=</span><span class="s">'65535'</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/nat&gt;</span>
  <span class="nt">&lt;/forward&gt;</span>
  <span class="nt">&lt;bridge</span> <span class="na">name=</span><span class="s">'virbr0'</span> <span class="na">stp=</span><span class="s">'on'</span> <span class="na">delay=</span><span class="s">'0'</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;mac</span> <span class="na">address=</span><span class="s">'52:54:00:9f:f9:f6'</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;ip</span> <span class="na">address=</span><span class="s">'192.168.122.1'</span> <span class="na">netmask=</span><span class="s">'255.255.255.0'</span><span class="nt">&gt;</span>
    <span class="nt">&lt;dhcp&gt;</span>
      <span class="nt">&lt;range</span> <span class="na">start=</span><span class="s">'192.168.122.2'</span> <span class="na">end=</span><span class="s">'192.168.122.254'</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/dhcp&gt;</span>
  <span class="nt">&lt;/ip&gt;</span>
<span class="nt">&lt;/network&gt;</span>
</code></pre></div></div>

<p>I’m going to change the range to <code class="language-plaintext highlighter-rouge">192.168.100.0/24</code> and the network name to <code class="language-plaintext highlighter-rouge">maikenet</code> since I like it more and tells me on the name what it is. You can remove UUID.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;network&gt;</span>
  <span class="nt">&lt;name&gt;</span>maikenet<span class="nt">&lt;/name&gt;</span>
  <span class="nt">&lt;forward</span> <span class="na">mode=</span><span class="s">'nat'</span><span class="nt">&gt;</span>
    <span class="nt">&lt;nat&gt;</span>
      <span class="nt">&lt;port</span> <span class="na">start=</span><span class="s">'1024'</span> <span class="na">end=</span><span class="s">'65535'</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/nat&gt;</span>
  <span class="nt">&lt;/forward&gt;</span>
  <span class="nt">&lt;bridge</span> <span class="na">name=</span><span class="s">'virbr0'</span> <span class="na">stp=</span><span class="s">'on'</span> <span class="na">delay=</span><span class="s">'0'</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;mac</span> <span class="na">address=</span><span class="s">'52:54:00:9f:f9:f6'</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;ip</span> <span class="na">address=</span><span class="s">'192.168.100.1'</span> <span class="na">netmask=</span><span class="s">'255.255.255.0'</span><span class="nt">&gt;</span>
    <span class="nt">&lt;dhcp&gt;</span>
      <span class="nt">&lt;range</span> <span class="na">start=</span><span class="s">'192.168.100.2'</span> <span class="na">end=</span><span class="s">'192.168.100.254'</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/dhcp&gt;</span>
  <span class="nt">&lt;/ip&gt;</span>
<span class="nt">&lt;/network&gt;</span>
</code></pre></div></div>

<p>Now let’s destroy the network interface and create a new one. Beware if you’re doing these commands while your machines are running and attached to any network you’re destroying, they might need a reboot to recover their IPs once that network is back up.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># To destroy it</span>
virsh net-destroy default

<span class="c"># We need to undefine it in case something is assigned to it already but also because we're not using it anymore</span>
virsh net-undefine default

<span class="c"># To recreate it from file</span>
virsh net-define mynetwork.xml

<span class="c"># Now start it and autostart to ensure it starts with NixOS</span>
virsh net-start maikenet
virsh net-autostart maikenet

<span class="c"># Check with an ifconfig</span>
ifconfig
<span class="c"># You should see an adapter virbr0 with the right IP</span>
</code></pre></div></div>

<h1 id="creating-a-cloud-init-enabled-image">Creating a cloud-init enabled image</h1>

<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">
    <p><strong>IMPORTANT</strong></p>

    <p>Always download the VM version, not the installer version from https://www.freebsd.org/where/</p>
  </div></div>

<h2 id="for-a-zfs-vm-image">For a ZFS VM image</h2>

<p>This will create a template on your PC to run cloud-init from the ZFS VM image that uses ZFS filesystem, <strong>the most common case.</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Make a folder for your vms</span>
<span class="nb">mkdir</span> <span class="nv">$HOME</span>/vms
<span class="nb">cd</span> <span class="nv">$HOME</span>/vms

<span class="c"># Download standard VM image and unzip it</span>
wget https://download.freebsd.org/releases/VM-IMAGES/14.3-RELEASE/amd64/Latest/FreeBSD-14.3-RELEASE-amd64-zfs.qcow2.xz

<span class="c"># Decompress but keeps the original</span>
xz <span class="nt">-dk</span> FreeBSD-14.3-RELEASE-amd64-zfs.qcow2.xz

<span class="c"># Make the disk slightly bigger</span>
<span class="nb">mv </span>FreeBSD-14.3-RELEASE-amd64-zfs.qcow2 freebsd14-cloud-init-zfs.qcow2
qemu-img resize freebsd14-cloud-init-zfs.qcow2 10G

<span class="c"># Run it with the network to install CloudInit</span>
virt-install <span class="se">\</span>
  <span class="nt">--name</span> freebsd-zfs <span class="se">\</span>
  <span class="nt">--memory</span> 2048 <span class="se">\</span>
  <span class="nt">--vcpus</span> 2 <span class="se">\</span>
  <span class="nt">--disk</span> <span class="nv">path</span><span class="o">=</span>freebsd14-cloud-init-zfs.qcow2,format<span class="o">=</span>qcow2,bus<span class="o">=</span>virtio <span class="se">\</span>
  <span class="nt">--os-variant</span> freebsd14.0 <span class="se">\</span>
  <span class="nt">--import</span> <span class="se">\</span>
  <span class="nt">--network</span> <span class="nv">network</span><span class="o">=</span>maikenet,model<span class="o">=</span>virtio <span class="se">\</span>
  <span class="nt">--graphics</span> spice
</code></pre></div></div>

<h2 id="now-inside-the-machine">Now inside the machine</h2>

<p>The default root user is passwordless so if you use <code class="language-plaintext highlighter-rouge">root</code> it won’t ask for any password, just log you in.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># OPTIONAL: Keyboard to Spanish, symbols are in different places</span>
kbdcontrol <span class="nt">-l</span> es

<span class="c"># Now inside the machine prepare it for cloud-init (as root, no pass)</span>
pkg update
pkg search cloud-init
pkg <span class="nb">install</span> <span class="nt">-y</span> WHATEVER_VERSION_YOU_GOT_FROM_SEARCH

<span class="c"># Now enable it</span>
sysrc <span class="nv">cloudinit_enable</span><span class="o">=</span><span class="s2">"YES"</span>
poweroff

<span class="c"># On your host system: Back it up</span>
xz <span class="nt">-k</span> freebsd14-cloud-init-zfs.qcow2
</code></pre></div></div>

<h1 id="using-your-own-templates-to-launch-custom-made-vms-easily">Using your own templates to launch custom-made VMs easily</h1>

<h2 id="create-a-cloud-init-config">Create a cloud-init config</h2>

<p>The SSH key I have there is the default one I have in SSH part of my home-manager config.</p>

<ol>
  <li>Create this file as <code class="language-plaintext highlighter-rouge">user-data.yaml</code> on the <code class="language-plaintext highlighter-rouge">$HOME/vms</code> folder as the basic template for all other machines:</li>
</ol>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#cloud-config</span>
<span class="na">hostname</span><span class="pi">:</span> <span class="s">freebsd1</span>
<span class="na">users</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">maikel</span>
    <span class="na">shell</span><span class="pi">:</span> <span class="s">/usr/local/bin/fish</span>
    <span class="na">sudo</span><span class="pi">:</span> <span class="s">ALL=(ALL) NOPASSWD:ALL</span>
    <span class="na">lock_passwd</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="c1"># Use mkpasswd -m sha-512 to get this</span>
    <span class="na">passwd</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$6$L80IKTwDwcfp......josH0"</span>
    <span class="na">ssh_authorized_keys</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ssh-ed25519 AAAA......ikel.dev</span>
<span class="na">ssh_pwauth</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">keyboard</span><span class="pi">:</span>
  <span class="na">layout</span><span class="pi">:</span> <span class="s">es</span>
<span class="na">packages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">fish</span>
  <span class="pi">-</span> <span class="s">sudo</span>
  <span class="pi">-</span> <span class="s">mkpasswd</span>
  <span class="pi">-</span> <span class="s">neovim</span>
  <span class="pi">-</span> <span class="s">ncdu</span>
  <span class="pi">-</span> <span class="s">git</span>
<span class="na">runcmd</span><span class="pi">:</span>
  <span class="c1"># Enable SSH</span>
  <span class="pi">-</span> <span class="s">sysrc sshd_enable=YES</span>
  <span class="pi">-</span> <span class="s">service sshd start</span>
  <span class="c1"># OPTIONAL: Set Spanish keyboard permanently</span>
  <span class="pi">-</span> <span class="s">sysrc keymap="es.kbd"</span>
  <span class="pi">-</span> <span class="s">service syscons restart</span>
  <span class="c1"># OPTIONAL: set root and maikel shells to fish explicitly</span>
  <span class="pi">-</span> <span class="s">pw usermod root -s /usr/local/bin/fish</span>
  <span class="pi">-</span> <span class="s">pw usermod maikel -s /usr/local/bin/fish</span>
  <span class="c1"># OPTIONAL: Auto resize main partition</span>
  <span class="pi">-</span> <span class="s">gpart recover vtbd0</span>
  <span class="pi">-</span> <span class="s">gpart resize -i 4 vtbd0</span>
  <span class="pi">-</span> <span class="s">zpool online -e zroot /dev/vtbd0p4</span>
</code></pre></div></div>

<ol>
  <li>Use this command to create a CD-ROM ISO to launch it from. Assuming you’re in <code class="language-plaintext highlighter-rouge">$HOME/vms</code>:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cloud-localds seed.iso user-data.yaml
</code></pre></div></div>

<h2 id="creating-the-final-machine-using-the-zfs-image">Creating the final machine using the ZFS image</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Access the folder of your vms</span>
<span class="nb">cd</span> <span class="nv">$HOME</span>/vms

<span class="c"># Decompress the fresh cloud-init-enabled version we created before</span>
xz <span class="nt">-dk</span> freebsd14-cloud-init-zfs.qcow2.xz

<span class="c"># Rename it to something more useful to distinguish it from the template</span>
<span class="nb">cp </span>freebsd14-cloud-init-zfs.qcow2 freebsd1.qcow2

<span class="c"># Create the seed ISO from user-data.yaml in case you've made any changes</span>
cloud-localds freebsd1.iso user-data.yaml

<span class="c"># Make the disk bigger here it is set to 20G but you can do whatever size you like</span>
qemu-img resize freebsd1.qcow2 50G

virt-install <span class="se">\</span>
  <span class="nt">--name</span> freebsd1 <span class="se">\</span>
  <span class="nt">--memory</span> 4096 <span class="se">\</span>
  <span class="nt">--vcpus</span> 4 <span class="se">\</span>
  <span class="nt">--disk</span> <span class="nv">path</span><span class="o">=</span>freebsd1.qcow2,format<span class="o">=</span>qcow2,bus<span class="o">=</span>virtio <span class="se">\</span>
  <span class="nt">--disk</span> <span class="nv">path</span><span class="o">=</span>freebsd1.iso,device<span class="o">=</span>cdrom <span class="se">\</span>
  <span class="nt">--os-variant</span> freebsd14.0 <span class="se">\</span>
  <span class="nt">--import</span> <span class="se">\</span>
  <span class="nt">--network</span> <span class="nv">network</span><span class="o">=</span>maikenet,model<span class="o">=</span>virtio <span class="se">\</span>
  <span class="nt">--graphics</span> spice <span class="se">\</span>
  <span class="nt">--noautoconsole</span>
</code></pre></div></div>

<p>And that’s it, your system should be up and running ready to be used. Because you enabled NSS and added your default SSH key (default in <code class="language-plaintext highlighter-rouge">.ssh/config</code>), you can just log into it with a simple:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh maikel@freebsd1
</code></pre></div></div>

<p>…once the machine finishes running all of its cloud-init script.</p>

<p><img src="/assets/images/2025/09/image-4.png" alt="Image" /></p>

<h1 id="extra-steps-for-your-own-sanity">Extra steps for your own sanity</h1>

<h2 id="resizing-the-partition-to-use-all-available-space-zfs">Resizing the partition to use all available space (ZFS)</h2>

<p>If you want your system to use all the available space in your qcow2 file after resizing it you’ll need some extra steps. This can all be added on the user-data template though which I did so you don’t need to.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ensure vtbd0 is the name of it</span>
gpart show

<span class="c"># Resize partition</span>
gpart recover vtbd0

<span class="c"># Get the slice or number of partition, in my case is 4</span>
gpart show

<span class="c"># This is assuming the slice is 4</span>
gpart resize <span class="nt">-i</span> 4 vtbd0

<span class="c"># Again the end "p4" depends on the slice number</span>
zpool online <span class="nt">-e</span> zroot /dev/vtbd0p4

<span class="c"># Check with</span>
zpool list
</code></pre></div></div>

<p>That’s all your machine is ready to use. If you ever need to change the size of the qcow2 file repeat those steps.</p>

<h2 id="autostart-this-machine-with-nixos">Autostart this machine with NixOS</h2>

<p>Run on the host machine:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>virsh autostart freebsd1
</code></pre></div></div>

<p>Otherwise to start manually:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>virsh start freebsd1
</code></pre></div></div>

<h2 id="detach-cloud-init-disk-just-in-case">Detach cloud-init disk just in case</h2>

<p>Normally cloud-init runs only once, but just to be sure on the host machine:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># To find the name of the ISO device, in my case "hda"</span>
virsh domblklist freebsd1

<span class="c"># To both remove it and ensure it never comes back after reboot</span>
virsh change-media freebsd1 hda <span class="nt">--eject</span> <span class="nt">--config</span> <span class="nt">--live</span>
</code></pre></div></div>

<h2 id="cloning-user-data">Cloning user data</h2>

<p>I create this mostly because I was considering the Zerotier one that is far below and realised I can kill two birds with one shot.</p>

<pre><code class="language-fish">function clone_user_data
  if test (count $argv) -ne 1
    echo "Usage: clone_user_data &lt;vmname&gt;"
    return 1
  end

  set NEWVM $argv[1]
  set BASE "$HOME/vms/user-data.yaml"
  set vm_dir $HOME/vms/in_use/$vm
  mkdir -p $vm_dir
  set OUT "$vm_dir/$NEWVM-user-data.yaml"

  if not test -f $BASE
    echo "Error: base file $BASE does not exist"
    return 1
  end

  # Replace hostname line
  sed "s/^hostname:.*/hostname: $NEWVM/" $BASE &gt; $OUT
  echo "Created config: $OUT"
end
</code></pre>

<h2 id="creating-machines-quickly-with-fish-function">Creating machines quickly with Fish function</h2>

<p>At the moment I separate creating the user-data file for that machine from creating it because precisely we might want to change what is installed on the machine. So this is how I normally do it now:</p>

<pre><code class="language-fish"># Assuming decompressed ready cloud image on ~/vms
# Create a user-data file for that machine
clone_user_data freebsd4

# Edit it
vi $HOME/vms/in_use/freebsd4/freebsd4-user-data.yaml

# Create the machine
create_vm freebsd4
</code></pre>

<p>Then create the machine:</p>

<pre><code class="language-fish">function create_vm
  if test (count $argv) -lt 1
    echo "Usage: createvm &lt;vm-name&gt;"
    return 1
  end

  set vm $argv[1]
  set vm_dir $HOME/vms/in_use/$vm
  cd $vm_dir

  echo "Copying template to VM disk..."
  cp $HOME/vms/freebsd14-cloud-init-zfs.qcow2 $vm.qcow2

  echo "Creating seed ISO..."
  cloud-localds $vm.iso $vm-user-data.yaml

  echo "Resizing disk..."
  qemu-img resize $vm.qcow2 20G

  echo "Launching VM..."
  virt-install \
    --connect qemu:///system \
    --name $vm \
    --memory 4096 \
    --vcpus 4 \
    --disk path=$vm.qcow2,format=qcow2,bus=virtio \
    --disk path=$vm.iso,device=cdrom \
    --os-variant freebsd14.0 \
    --import \
    --network network=maikenet,model=virtio \
    --graphics spice \
    --noautoconsole

  echo "VM $vm launched."
end
</code></pre>

<h2 id="destroying-machines-quickly-with-fish-shell">Destroying machines quickly with Fish shell</h2>

<pre><code class="language-fish">function destroy_vm
  if test (count $argv) -lt 1
    echo "Usage: destroyvm &lt;vm-name&gt;"
    return 1
  end

  set vm $argv[1]
  echo "Destroying VM $vm..."
  virsh destroy $vm

  echo "Undefining VM $vm..."
  virsh undefine $vm

  set disk ~/vms/in_use/$vm/$vm.qcow2
  set seed ~/vms/in_use/$vm/$vm.iso
  set userdata ~/vms/in_use/$vm/$vm-user-data.yaml

  if test -f $disk
    echo "Deleting disk $disk..."
    rm -f $disk
  else
    echo "Disk $disk not found, skipping."
  end

  if test -f $seed
    echo "Deleting seed $seed..."
    rm -f $seed
  else
    echo "Disk $seed not found, skipping."
  end

  if test -f $userdata
    echo "Deleting user-data $userdata..."
    rm -f $userdata
  else
    echo "Disk $userdata not found, skipping."
  end

  rm -rf $HOME/vms/in_use/$vm
end
</code></pre>

<h2 id="zerotier-on-creation-with-self-authorisation">Zerotier on creation with self-authorisation</h2>

<p>This is something I’m experimenting with, installing Zerotier and joining a network are easy steps but I want it to self-authorise too. It does works as it currently is but I want the variables to be fed into the cloud config somehow instead of hard-coding the variables.</p>

<p>Then once the machine is up and running you can just and simply run <code class="language-plaintext highlighter-rouge">/root/join-network.sh</code> as root.</p>

<p>I set a few variables to simplify this all, for example I want the fish functions to be in the vm folder as they are all related to this.</p>

<pre><code class="language-fish"># This loads functions from the path in the vms add to config.fish
set -g fish_function_path $fish_function_path ~/vms/fish_functions

# This sets the default password I want to use for my machines which is later hashed by mkpasswd, can be set from the shell
set -Ua DEFAULT_VM_PASSWORD whatever_passw_you_want
</code></pre>

<p>I also did a few more changes here and in the cloning function since now this is my standard user-data.yaml template:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#cloud-config</span>
<span class="na">hostname</span><span class="pi">:</span> 
<span class="na">users</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">maikel</span>
    <span class="na">shell</span><span class="pi">:</span> <span class="s">/usr/local/bin/fish</span>
    <span class="na">sudo</span><span class="pi">:</span> <span class="s">ALL=(ALL) NOPASSWD:ALL</span>
    <span class="na">lock_passwd</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">passwd</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
    <span class="na">ssh_authorized_keys</span><span class="pi">:</span>
      <span class="pi">-</span> 
<span class="na">ssh_pwauth</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">keyboard</span><span class="pi">:</span>
  <span class="na">layout</span><span class="pi">:</span> <span class="s">es</span>
<span class="na">packages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">fish</span>
  <span class="pi">-</span> <span class="s">sudo</span>
  <span class="pi">-</span> <span class="s">mkpasswd</span>
  <span class="pi">-</span> <span class="s">neovim</span>
  <span class="pi">-</span> <span class="s">ncdu</span>
  <span class="pi">-</span> <span class="s">zerotier</span>
  <span class="pi">-</span> <span class="s">curl</span>
  <span class="pi">-</span> <span class="s">jq</span>
<span class="na">write_files</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/root/join-network.sh</span>
    <span class="na">permissions</span><span class="pi">:</span> <span class="s1">'</span><span class="s">0755'</span>
    <span class="na">content</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">#!/bin/sh</span>
      <span class="s">zerotier-cli join "" &amp;&amp; \</span>
      <span class="s">MEMBER_ID=$(zerotier-cli info | awk '{print $3}') &amp;&amp; \</span>
      <span class="s">curl -H "Authorization: token " -X POST \</span>
      <span class="s">"https://api.zerotier.com/api/v1/network//member/$MEMBER_ID" \</span>
      <span class="s">--data '{"config": {"authorized": true}}'</span>
<span class="na">runcmd</span><span class="pi">:</span>
  <span class="c1"># Enable SSH</span>
  <span class="pi">-</span> <span class="s">sysrc sshd_enable=YES</span>
  <span class="pi">-</span> <span class="s">service sshd start</span>
  <span class="c1"># Set Spanish keyboard permanently</span>
  <span class="pi">-</span> <span class="s">sysrc keymap="es.kbd"</span>
  <span class="pi">-</span> <span class="s">service syscons restart</span>
  <span class="c1"># Optional: set root and maikel shells to fish explicitly</span>
  <span class="pi">-</span> <span class="s">pw usermod root -s /usr/local/bin/fish</span>
  <span class="pi">-</span> <span class="s">pw usermod maikel -s /usr/local/bin/fish</span>
  <span class="c1"># Auto resize main partition</span>
  <span class="pi">-</span> <span class="s">gpart recover vtbd0</span>
  <span class="pi">-</span> <span class="s">gpart resize -i 4 vtbd0</span>
  <span class="pi">-</span> <span class="s">zpool online -e zroot /dev/vtbd0p4</span>
  <span class="c1"># Zerotier joy</span>
  <span class="pi">-</span> <span class="s">sysrc zerotier_enable="YES"</span>
  <span class="pi">-</span> <span class="s">service zerotier start</span>
</code></pre></div></div>

<p>Applying the ZT_TOKEN and ZT_NW with a Fish-shell function <code class="language-plaintext highlighter-rouge">clone_user_data VM_NAME</code>:</p>

<pre><code class="language-fish">function clone_user_data
  if test (count $argv) -ne 1
    echo "Usage: clone_user_data &lt;vmname&gt;"
    return 1
  end

  set NEWVM $argv[1]
  set BASE "$HOME/vms/user-data.yaml"
  set VM_DIR "$HOME/vms/in_use/$NEWVM"
  mkdir -p "$VM_DIR"
  echo "Created directory $VM_DIR"

  set OUT "$VM_DIR/$NEWVM-user-data.yaml"

  if not test -f $BASE
    echo "Error: base file $BASE does not exist"
    return 1
  end

  if test -z "$ZT_TOKEN"
    echo "Error: ZT_TOKEN environment variable not set"
    return 1
  end

  if test -z "$ZT_NWID"
    echo "Error: ZT_NWID environment variable not set"
    return 1
  end

  if test -z "$DEFAULT_VM_PASSWORD"
    echo "Error: DEFAULT_VM_PASSWORD environment variable not set"
    return 1
  end

  # Generate hashed password
  set PASSWD (mkpasswd -m sha-512 $DEFAULT_VM_PASSWORD)

  # Extract default identity file from ssh config (already a .pub in your setup)
  set PUBKEYFILE (grep -m1 -i 'IdentityFile' ~/.ssh/config | awk '{print $2}' | sed "s|~|$HOME|")

  if test -z "$PUBKEYFILE"
    echo "Error: could not find IdentityFile in ~/.ssh/config"
    return 1
  end

  if not test -f $PUBKEYFILE
    echo "Error: public key $PUBKEYFILE not found"
    return 1
  end

  set PUBKEY (cat $PUBKEYFILE)

  sed \
    -e "s||$NEWVM|g" \
    -e "s||$ZT_NWID|g" \
    -e "s||$ZT_TOKEN|g" \
    -e "s||$PASSWD|g" \
    -e "s||$PUBKEY|" \
    $BASE &gt;$OUT

  if test $status -ne 0
    echo "Error: failed to generate $OUT"
    return 1
  end

  echo "Created config: $OUT"
end
</code></pre>

<h2 id="installing-a-desktop-environment">Installing a desktop environment</h2>

<p>I use KDE, it really just needs to read the handbook and follow it step by step. I even created its own <code class="language-plaintext highlighter-rouge">desktop-user-data.yaml</code> file for this one in case I ever need the desktop.</p>

<p>The yaml file for cloudinit:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#cloud-config</span>
<span class="na">hostname</span><span class="pi">:</span> <span class="s">desktop</span>
<span class="na">users</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">maikel</span>
    <span class="na">shell</span><span class="pi">:</span> <span class="s">/usr/local/bin/fish</span>
    <span class="na">sudo</span><span class="pi">:</span> <span class="s">ALL=(ALL) NOPASSWD:ALL</span>
    <span class="na">lock_passwd</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">passwd</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$6$L80IKTw............osH0"</span>
    <span class="na">ssh_authorized_keys</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ssh-ed25519 ....... maikel.dev</span>
<span class="na">ssh_pwauth</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">keyboard</span><span class="pi">:</span>
  <span class="na">layout</span><span class="pi">:</span> <span class="s">es</span>
<span class="na">packages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">fish</span>
  <span class="pi">-</span> <span class="s">sudo</span>
  <span class="pi">-</span> <span class="s">mkpasswd</span>
  <span class="pi">-</span> <span class="s">neovim</span>
  <span class="pi">-</span> <span class="s">ncdu</span>
  <span class="pi">-</span> <span class="s">xorg</span>
  <span class="pi">-</span> <span class="s">kde</span>
  <span class="pi">-</span> <span class="s">sddm</span>
<span class="na">runcmd</span><span class="pi">:</span>
  <span class="c1"># Enable SSH</span>
  <span class="pi">-</span> <span class="s">sysrc sshd_enable=YES</span>
  <span class="pi">-</span> <span class="s">service sshd start</span>
  <span class="c1"># Set Spanish keyboard permanently</span>
  <span class="pi">-</span> <span class="s">sysrc keymap="es.kbd"</span>
  <span class="pi">-</span> <span class="s">service syscons restart</span>
  <span class="c1"># Optional: set root and maikel shells to fish explicitly</span>
  <span class="pi">-</span> <span class="s">pw usermod root -s /usr/local/bin/fish</span>
  <span class="pi">-</span> <span class="s">pw usermod maikel -s /usr/local/bin/fish</span>
  <span class="c1"># Auto resize main partition</span>
  <span class="pi">-</span> <span class="s">gpart recover vtbd0</span>
  <span class="pi">-</span> <span class="s">gpart resize -i 4 vtbd0</span>
  <span class="pi">-</span> <span class="s">zpool online -e zroot /dev/vtbd0p4</span>
  <span class="c1"># Add KDE</span>
  <span class="pi">-</span> <span class="s">pw groupmod video -m maikel</span>
  <span class="pi">-</span> <span class="s">sysrc dbus_enable="YES"</span>
  <span class="pi">-</span> <span class="s">service dbus start</span>
  <span class="pi">-</span> <span class="s">sysctl net.local.stream.recvspace=65536</span>
  <span class="pi">-</span> <span class="s">sysctl net.local.stream.sendspace=65536</span>
  <span class="pi">-</span> <span class="s">sysctl -f /etc/sysctl.conf</span>
  <span class="pi">-</span> <span class="s">sysrc sddm_enable="YES"</span>
  <span class="pi">-</span> <span class="s">sysrc sddm_lang="es_ES"</span>
  <span class="pi">-</span> <span class="s">setxkbmap -layout es</span>
  <span class="pi">-</span> <span class="s">service ssdm start</span>
</code></pre></div></div>

<p>The machine has a few differences, I assigned more total system memory (8GB) and hiked the RAM assigned to the video card too. My mouse is a USB one so only works with that line in input. If yours isn’t USB delete that line.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>virt-install <span class="se">\</span>
  <span class="nt">--connect</span> qemu:///system <span class="se">\</span>
  <span class="nt">--name</span> desktop <span class="se">\</span>
  <span class="nt">--memory</span> 8192 <span class="se">\</span>
  <span class="nt">--vcpus</span> 4 <span class="se">\</span>
  <span class="nt">--disk</span> <span class="nv">path</span><span class="o">=</span>desktop.qcow2,format<span class="o">=</span>qcow2,bus<span class="o">=</span>virtio <span class="se">\</span>
  <span class="nt">--disk</span> <span class="nv">path</span><span class="o">=</span>desktop.iso,device<span class="o">=</span>cdrom <span class="se">\</span>
  <span class="nt">--os-variant</span> freebsd14.0 <span class="se">\</span>
  <span class="nt">--import</span> <span class="se">\</span>
  <span class="nt">--video</span> qxl,ram<span class="o">=</span>524288,vram<span class="o">=</span>262144,vgamem<span class="o">=</span>262144 <span class="se">\</span>
  <span class="nt">--network</span> <span class="nv">network</span><span class="o">=</span>maikenet,model<span class="o">=</span>virtio,mac<span class="o">=</span>52:54:00:6f:3c:58 <span class="se">\</span>
  <span class="nt">--input</span> <span class="nb">type</span><span class="o">=</span>mouse,bus<span class="o">=</span>usb <span class="se">\</span>
  <span class="nt">--graphics</span> spice <span class="se">\</span>
  <span class="nt">--noautoconsole</span>
</code></pre></div></div>

<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">
    <p>The Fish function <strong>create_vm</strong> won’t work for this because the definition is for a non-GUI machine. But you can use the Fish function <strong>clone_user_data</strong> to get the MAC and fix the IP. Since this was a one off, I didn’t care about automating it. As soon as I discovered the <strong>hours-long</strong> nightmare that is <a href="https://freebsdfoundation.org/resource/how-to-use-vs-code-on-freebsd/">installing VSCode in FreeBSD</a> I realised I’m only using it for servers and appliances.</p>
  </div></div>

<h2 id="cleaning-up">Cleaning up</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># See the machines</span>
<span class="nb">sudo </span>virsh list <span class="nt">--all</span>

<span class="c"># The first stops immediately the machine</span>
<span class="nb">sudo </span>virsh destroy freebsd14

<span class="c"># This second removes it from the pool of VMs of libvirtd</span>
<span class="nb">sudo </span>virsh undefine freebsd14

<span class="c"># Delete any pre-made seed just in case</span>
<span class="nb">rm</span> <span class="nt">-rf</span> seed.iso
</code></pre></div></div>

<h1 id="extra-repo-with-all-this">Extra: Repo with all this</h1>

<p>I made a repo with all these commands including a pre-made ZFS-ready FreeBSD 14.3 image.</p>

<p>The URL is <a href="https://github.com/maikelthedev/libvirtd_automation">https://github.com/maikelthedev/libvirtd_automation</a></p>

<h1 id="some-oddities">Some oddities</h1>

<p>These are some painful parts from the process.</p>

<h2 id="the-command-virt-install-and-">The command <code class="language-plaintext highlighter-rouge">virt-install</code> and “~”</h2>

<p>I don’t know why the path can’t interpret “~” hence why I did it all from the <code class="language-plaintext highlighter-rouge">$HOME/vms</code> folder. In this version I changed “~” to <code class="language-plaintext highlighter-rouge">$HOME</code> in all scripts for consistency.</p>

<h2 id="run-without-virt-viewer">Run without virt-viewer</h2>

<p>Sometimes you want to install and see nothing, in those case use:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">--graphics</span> spice <span class="se">\</span>
<span class="nt">--noautoconsole</span>
</code></pre></div></div>

<p>At the end of the <code class="language-plaintext highlighter-rouge">virt-install</code> command, this runs the system with graphics enable but doesn’t attach any viewer to it.</p>]]></content><author><name>Maikel Frias Mosquea</name></author><category term="FreeBSD" /><category term="NixOS" /><category term="DevOps" /><category term="virtualization" /><summary type="html"><![CDATA[Complete guide to running FreeBSD alongside NixOS using libvirtd, with automation scripts, network configuration, cloud-init setup, and desktop environment installation.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.maikel.dev/assets/images/unsplash-1657724576853.jpg" /><media:content medium="image" url="https://blog.maikel.dev/assets/images/unsplash-1657724576853.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>