<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Bastian Gruber</title>
  <subtitle></subtitle>
  <link href="https://bastiangruber.ca/feed.xml" rel="self"/>
  <link href="https://bastiangruber.ca/"/>
  
    <updated>2026-02-24T00:00:00Z</updated>
  
  <id>https://bastiangruber.ca</id>
  <author>
    <name>Bastian Gruber</name>
    <email>info@bastiangruber.ca</email>
  </author>
  
    
    <entry>
      <title>I freaking love the new tools I built for myself</title>
      <link href="https://bastiangruber.ca/posts/i-freaking-love-the-new-tools-i-built-for-myself/"/>
      <updated>2026-02-24T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/i-freaking-love-the-new-tools-i-built-for-myself/</id>
      <content type="html">
        <![CDATA[
      <h2>How It All Started</h2>
<p>My entry into programming was at 16, when friend came over to my house and told me about one of our neighbours, who was building websites, and even selling them (can you believe that?!) to small businesses. We looked at the websites and thought: We can do that better.</p>
<p>The way I entered the field might be how I always act, or it defined how I approach this craft: I want to build solutions to problems. I was never the one who could dabble deep into programming languages and nerd out on syntax or object-oriented vs. functional etc. For me, the endgoal was always priority. With more time in the field, I also cared about HOW I build these solutions, and HOW the code and structure looks like.</p>
<p>A few years forward, and I found myself in the StartUp world in Berlin. I loved the pace, the atmosphere, and the building, pivoting, trying out new tools to solve problems faster and better. Working at a company didn't satisfy my lust for exploring, so I wanted to develop my ideas outside of work. The city always provided enough UX/UI people to partner and tinker with. None of the toy projects ever succeeded, but that was not the point, really.</p>
<h2>Family Meets LLMs</h2>
<p>Life changes, I have a family now, went through the first hard years of being a parent, and I slowly feel more free time to explore this lust of building and solving problems. Now, in this phase, the tech ecosystem throws new tools my way to help me exactly with that: LLMs.</p>
<p>Was it more or less &quot;meh&quot; in the beginning, since Claude Opus 4.5, I can really thrive.</p>
<h2>The Pool Dashboard</h2>
<p>For example, we have a local community pool where I live now, and there website is hard to read, and the schedule hard to decipher. I am part of a parent WhatsApp group and the complains were every week the same: &quot;I stood in front of the pool and it was closed&quot;, &quot;Can someone tell me if the pool is actually open?&quot;.</p>
<p>Well, on one of these afternoons, between doing the dishes, prepping the day for my Kids, I threw the problem into Claude, gave it the website with the hard-to-read schedule, and told it to build a very basic, bare bones dashboard with YES/NO if the pool is open. I didn't look at the website yet, I just wanted a prototype and a first idea if it works. 5 minutes later it found the API call to for the JSON the website is getting, and build a very basic HTML/CSS website. I fed it more features, saw more edge cases, adjusted the code, gave it more guidelines. And fast forward one hour later, instead of answering to the WhatsApp group &quot;I think it is&quot;, <a href="https://isthelclcpoolopen.ca/">I posted a link for the website</a>. While Claude was working, I purchased the domain, set up a GitHub CI/CD pipeline, set up GitHub pages, and then merged it all together.</p>
<p>It was fun, exhilarting, and reminded me of the pure joy I felt when I was younger. Could I have done it myself? Oh absolutely. But not in 45mins while I had to do 5 different things. For these debugging sessions, I need a notebook, a quiet place to think and bounce back and forth until I have a working example. Fun if you have all the time in the world, not so fun if you have 2 kids in the background who have priority (at least, most of the time).</p>
<h2>The NHL Playoff Dashboard</h2>
<p>The list goes on. A few months later, friends of mine wanted to start a NHL Playoff Fantasy team. We picked the teams, our friend forwarded the free website he set it up. And it was - aweful. I thought: I can do it better! Span up Claude inside Zed, and with the help of GitHub repos for unofficial NHL API documentation, span up a <a href="https://fantasy-frontend.fly.dev/">fully functional, live score and live data NHL dashboard</a> for the playoffs, with each of our teams and players. It took about a week to get right, but without the help of the LLM, I would have taken so much longer (and probably lost interest) in setting up all the edge cases, details, parse API responses which were not documented etc.</p>
<h2>Building Tools for Every Day</h2>
<p>With the time, the tools got better, and I can finally build tools I can - and want to - use every day. I always wanted a seamless writing experience in the browser without logins and complex UI. So I built <a href="https://workledger.org/">WorkLedger</a>. I am using it every day now for a week and it truly changed my workday. Afer using it even more than I expected, I build a backend-sync server, and could nerd out with a close to <a href="https://bastiangruber.ca/posts/how-i-built-a-minimal-knowledge-sync-for-workledger/">&quot;zero-knowledge&quot; sync</a> I always wanted to try out. The sheer fun of using something every day, and it works magically behind the scenes.</p>
<p>After that, I was tired of keeping my Brag Document in a Google Doc, so I build a fully customizable work stream &quot;collector&quot; called <a href="https://brag-frog.org/">Brag Frog</a>, which syncs all my resources into one place, and I can prep meetings, add items to OKRs and let myself draft a self review every half year based on all the work I am doing from across 7 different platforms.</p>
<p>I can open source this work, it runs fairly cheap on a fly.io instance. The static websites either run on GitHub or Vercel. It is fast and easy to setup and ship.</p>
<h2>What About Quality?</h2>
<p>And for the quality? My god, you can really fine tune and throw stuff at Claude Opus 4.6. The <a href="https://github.com/gruberb/brag-frog">Brag Fox project</a> particual was a mess. I constantly added and refined features, and the code ended up like expected: Messy. But hey, I wrote down a plan, guidelines how a proper code structure should look like for this type of project. Added guidelines from DDD and let it work. It took 2h, and 25 different tasks, and it refactored the whole codebase.</p>
<p>Is it perfect? Hell no. But so wouldn't be my code for a side project I build. I would even say, it is way better than what I would have come up with. Why? Becasue I build it on the side. I can now focus on guidelines and best practices, let it run for hours, and the solution is so close to &quot;very good&quot;, that I can manually go through each module and do adjustments if I feel like it. But often times: Really not needed.</p>
<h2>Why it works so well for me</h2>
<p>I am a very pragmatic person. I need to see and touch things to understand them. I can read tutorials or study codebases, but I need to get my hands dirty. Spinning something up, jumping between RFC docs and then building it out, seeing if it works, and then going back to higher-level thinking: That's the stuff I love. I can do that now every time I like. And even with problems at work: I can spin up example implementations, see if they make sense, and then either throw them out or iterate until they meet my quality standards. All the while learning a ton of new things along the way.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>How I built a minimal-knowledge sync for WorkLedger</title>
      <link href="https://bastiangruber.ca/posts/how-i-built-a-minimal-knowledge-sync-for-workledger/"/>
      <updated>2026-02-20T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/how-i-built-a-minimal-knowledge-sync-for-workledger/</id>
      <content type="html">
        <![CDATA[
      <h2>A local-first digital notebook</h2>
<p>A bit more than a week ago, I came across the blog post <a href="https://ntietz.com/blog/using-an-engineering-notebook/">Using an engineering notebook</a>. I personally love notebooks and use... too many of them. My only issue has always been that when my brain is on fire, my handwriting is not neat enough, it's not structured enough, and it helps me to note things down, but it always feels like I could be faster by text-to-speech or keyboard typing.</p>
<p>Now there are moments of silence of course, when I can sit down with a cup of coffee and map out a feature or thought in my head. For that, I don't think any digital device will ever come close to simple pen and paper.</p>
<p>Nevertheless, the time was ripe to &quot;Build it yourself&quot;. I am always inspired by <a href="https://excalidraw.com/">Excalidraw</a>. I can open it, don't need an account, draw to my heart's content, and then save the finished drawing as a PNG to my local hard drive and take it anywhere. I wanted to build something seamless like that for writing. Hence, <a href="https://workledger.org/">WorkLedger was born</a>.</p>
<blockquote>
<p>The <a href="https://about.workledger.org" target="_blank">landing page</a> gives you a nice feature overview.</p>
</blockquote>
<blockquote>
<p>Shout out to <a href="https://www.blocknotejs.org/">BlockNote</a> for offering a superb text editing experience.</p>
</blockquote>
<p>I thought to myself: Local first is great, I will never need sync. Until I left the house on a Friday to bring my kids to skating lessons, work still ruminating within me, and I thought: Let's get this down on &quot;paper&quot; so I don't forget my thoughts. I opened WorkLedger on my iPhone and saw - nothing. Obviously a new session means new data. Argh. I had to implement sync.</p>
<div class="sidecar"> A note: I am not a security expert. If you find issues or concerns with this workflow, feel free to email me at foreach [at] me dot com.</div>
<h2>The birth of sync ID</h2>
<p>I started using <a href="https://mullvad.net/en">Mullvad VPN</a> this past year and was utterly confused by their lack of... account? I couldn't find a deep dive into this, all I have as an explanation is <a href="https://mullvad.net/en/blog/mullvads-account-numbers-get-longer-and-safer">this blog post</a> from 2017.</p>
<p>I got inspired though: What if I had a button which can generate a sync ID, and with that, I can - sync? That's it. No password, no complicated setup. And I want to copy/paste this sync ID to my devices where I need it. And <em>somehow</em>, the server doesn't even know this id, but still can give me my associated entries.</p>
<h3>The core trick: domain separation</h3>
<p>The core question is: If the server never sees this magic id, how does it know which entries belong to me?</p>
<p>The answer is that one string becomes two different strings through a one-way process. Given a sync ID like <code>wl-a1b2c3d4e5f6a7b8c9d0</code>, I derive two separate values from it:</p>
<p><img src="/images/domain_separation.png" alt="Domain separation"></p>
<p>This is called <a href="https://github.com/SalusaSecondus/CryptoGotchas/blob/master//domain_separation.md">domain separation</a>. The prefixes <code>auth</code> and <code>crypto</code> ensure the two hashes are computationally unrelated — knowing one reveals nothing useful about the other.</p>
<p>The <code>auth</code> token goes to the server as an <code>X-Auth-Token</code> header. The server uses it to identify your account and look up your entries. But because it is a SHA-256 hash of a sync ID (not the sync ID itself), the server cannot reverse it. And because the crypto seed uses a different prefix, the server cannot derive the encryption key either.</p>
<p>This is how it looks in my <code>crypto.ts</code> file:</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">sha256Hex</span><span class="token punctuation">(</span>input<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> encoded <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TextEncoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">encode</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> hash <span class="token operator">=</span> <span class="token keyword">await</span> crypto<span class="token punctuation">.</span>subtle<span class="token punctuation">.</span><span class="token function">digest</span><span class="token punctuation">(</span><span class="token string">"SHA-256"</span><span class="token punctuation">,</span> encoded<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> <span class="token builtin">Array</span><span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span>hash<span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span>b<span class="token punctuation">)</span> <span class="token operator">=></span> b<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">padStart</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">computeAuthToken</span><span class="token punctuation">(</span>syncId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token function">sha256Hex</span><span class="token punctuation">(</span><span class="token string">"auth:"</span> <span class="token operator">+</span> syncId<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">computeCryptoSeed</span><span class="token punctuation">(</span>syncId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token function">sha256Hex</span><span class="token punctuation">(</span><span class="token string">"crypto:"</span> <span class="token operator">+</span> syncId<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The <code>sha256Hex</code> function uses the Web Crypto API, which is available in all modern browsers, so I don't even need to import any npm modules for it.</p>
<h2>Generating the sync ID</h2>
<p>The sync ID is generated client-side from 10 random bytes, giving 80 bits of entropy.</p>
<blockquote>
<p>A byte can hold any value from 0 to 255 — that is 256 possibilities. Since each bit doubles the possibilities and
2⁸ = 256, one byte equals 8 bits of entropy. Ten random bytes means 10 × 8 = 80 bits.</p>
</blockquote>
<p>For reference:</p>
<ul>
<li>A typical password might have 30-50 bits of entropy (guessable)</li>
<li>80 bits is in the &quot;nation-state can't brute-force this&quot; range</li>
<li>128 bits is the standard target for symmetric cryptography</li>
</ul>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">generateSyncIdLocal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> bytes <span class="token operator">=</span> crypto<span class="token punctuation">.</span><span class="token function">getRandomValues</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> hex <span class="token operator">=</span> <span class="token builtin">Array</span><span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>bytes<span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span>b<span class="token punctuation">)</span> <span class="token operator">=></span> b<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">padStart</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">wl-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>hex<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The result looks like <code>wl-a1b2c3d4e5f6a7b8c9d0</code>. The <code>wl-</code> prefix is cosmetic - it makes the string recognizable as a WorkLedger sync ID when you paste it between devices.</p>
<p>80 bits of randomness means there are roughly 1.2 × 10²⁴ possible sync IDs. For any realistic number of users — say, 10 million accounts — the probability of a collision is vanishingly small.</p>
<h3>From crypto seed to encryption key</h3>
<p>The crypto seed alone is not the encryption key. It goes through one more transformation: <a href="https://www.ssltrust.ca/blog/pbkdf2-password-key-derivation">PBKDF2 key derivation</a> with a server-provided salt.</p>
<p>When you create an account, the server generates 16 random bytes as your salt and sends it back. This salt is stored alongside your auth token in the server database. Every device that connects with the same sync ID gets the same salt, which means every device derives the same encryption key.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> <span class="token constant">PBKDF2_ITERATIONS</span> <span class="token operator">=</span> <span class="token number">100_000</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">deriveKey</span><span class="token punctuation">(</span>syncId<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> saltBase64<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span>CryptoKey<span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> cryptoSeed <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">computeCryptoSeed</span><span class="token punctuation">(</span>syncId<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> encoder <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TextEncoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> salt <span class="token operator">=</span> Uint8Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token function">atob</span><span class="token punctuation">(</span>saltBase64<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>c<span class="token punctuation">)</span> <span class="token operator">=></span> c<span class="token punctuation">.</span><span class="token function">charCodeAt</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> keyMaterial <span class="token operator">=</span> <span class="token keyword">await</span> crypto<span class="token punctuation">.</span>subtle<span class="token punctuation">.</span><span class="token function">importKey</span><span class="token punctuation">(</span><br>    <span class="token string">"raw"</span><span class="token punctuation">,</span><br>    encoder<span class="token punctuation">.</span><span class="token function">encode</span><span class="token punctuation">(</span>cryptoSeed<span class="token punctuation">)</span><span class="token punctuation">,</span><br>    <span class="token string">"PBKDF2"</span><span class="token punctuation">,</span><br>    <span class="token boolean">false</span><span class="token punctuation">,</span><br>    <span class="token punctuation">[</span><span class="token string">"deriveKey"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> crypto<span class="token punctuation">.</span>subtle<span class="token punctuation">.</span><span class="token function">deriveKey</span><span class="token punctuation">(</span><br>    <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"PBKDF2"</span><span class="token punctuation">,</span> salt<span class="token punctuation">,</span> iterations<span class="token operator">:</span> <span class="token constant">PBKDF2_ITERATIONS</span><span class="token punctuation">,</span> hash<span class="token operator">:</span> <span class="token string">"SHA-256"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    keyMaterial<span class="token punctuation">,</span><br>    <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"AES-GCM"</span><span class="token punctuation">,</span> length<span class="token operator">:</span> <span class="token number">256</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token boolean">false</span><span class="token punctuation">,</span><br>    <span class="token punctuation">[</span><span class="token string">"encrypt"</span><span class="token punctuation">,</span> <span class="token string">"decrypt"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The output is an AES-256-GCM key - a symmetric key that both encrypts and decrypts. PBKDF2 with 100,000 iterations makes brute-force attacks against weak sync IDs expensive, though with 80 bits of entropy the sync ID itself is already computationally infeasible to guess.</p>
<p>The full derivation chain looks like this:</p>
<p><img src="/images/derive_aes_key.png" alt="Deriving the AES key"></p>
<h3>Encrypting entries</h3>
<p>With the key derived, each notebook entry gets encrypted before it leaves the device. I split each entry into two parts: the content payload that gets encrypted, and minimal metadata that stays in plaintext.</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">encryptEntry</span><span class="token punctuation">(</span>key<span class="token operator">:</span> CryptoKey<span class="token punctuation">,</span> entry<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span>SyncEntry<span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> payload <span class="token operator">=</span> <span class="token punctuation">{</span><br>    dayKey<span class="token operator">:</span> entry<span class="token punctuation">.</span>dayKey<span class="token punctuation">,</span><br>    createdAt<span class="token operator">:</span> entry<span class="token punctuation">.</span>createdAt<span class="token punctuation">,</span><br>    updatedAt<span class="token operator">:</span> entry<span class="token punctuation">.</span>updatedAt<span class="token punctuation">,</span><br>    blocks<span class="token operator">:</span> entry<span class="token punctuation">.</span>blocks<span class="token punctuation">,</span><br>    isArchived<span class="token operator">:</span> entry<span class="token punctuation">.</span>isArchived<span class="token punctuation">,</span><br>    tags<span class="token operator">:</span> entry<span class="token punctuation">.</span>tags <span class="token operator">??</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>    isPinned<span class="token operator">:</span> entry<span class="token punctuation">.</span>isPinned <span class="token operator">??</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br>    signifier<span class="token operator">:</span> entry<span class="token punctuation">.</span>signifier<span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> plaintext <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>payload<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> encryptedPayload <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">encrypt</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> plaintext<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    id<span class="token operator">:</span> entry<span class="token punctuation">.</span>id<span class="token punctuation">,</span>                    <span class="token comment">// plaintext — server needs for dedup</span><br>    updatedAt<span class="token operator">:</span> entry<span class="token punctuation">.</span>updatedAt<span class="token punctuation">,</span>      <span class="token comment">// plaintext — server needs for conflict resolution</span><br>    isArchived<span class="token operator">:</span> entry<span class="token punctuation">.</span>isArchived<span class="token punctuation">,</span>    <span class="token comment">// plaintext — server needs for filtering</span><br>    isDeleted<span class="token operator">:</span> entry<span class="token punctuation">.</span>isDeleted<span class="token punctuation">,</span>      <span class="token comment">// plaintext — server needs for deletion markers</span><br>    encryptedPayload<span class="token punctuation">,</span>                <span class="token comment">// opaque blob</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The content fields — your actual writing (<code>blocks</code>), your tags, your date organization (<code>dayKey</code>) — all go into the encrypted blob. The server gets only an opaque base64 string.</p>
<p>The metadata fields stay in plaintext because the server needs them for operational purposes: <code>id</code> for deduplication, <code>updatedAt</code> for last-write-wins conflict resolution, <code>isDeleted</code> for soft-delete markers.</p>
<p>The encryption itself uses AES-256-GCM with a random 12-byte initialization vector (IV) prepended to the ciphertext:</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">encrypt</span><span class="token punctuation">(</span>key<span class="token operator">:</span> CryptoKey<span class="token punctuation">,</span> plaintext<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> encoder <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TextEncoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> iv <span class="token operator">=</span> crypto<span class="token punctuation">.</span><span class="token function">getRandomValues</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span><span class="token number">12</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> ciphertext <span class="token operator">=</span> <span class="token keyword">await</span> crypto<span class="token punctuation">.</span>subtle<span class="token punctuation">.</span><span class="token function">encrypt</span><span class="token punctuation">(</span><br>    <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"AES-GCM"</span><span class="token punctuation">,</span> iv <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    key<span class="token punctuation">,</span><br>    encoder<span class="token punctuation">.</span><span class="token function">encode</span><span class="token punctuation">(</span>plaintext<span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> combined <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span>iv<span class="token punctuation">.</span>byteLength <span class="token operator">+</span> ciphertext<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  combined<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>iv<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  combined<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span>ciphertext<span class="token punctuation">)</span><span class="token punctuation">,</span> iv<span class="token punctuation">.</span>byteLength<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">let</span> binary <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> combined<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    binary <span class="token operator">+=</span> String<span class="token punctuation">.</span><span class="token function">fromCharCode</span><span class="token punctuation">(</span>combined<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br>  <span class="token keyword">return</span> <span class="token function">btoa</span><span class="token punctuation">(</span>binary<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The loop builds the binary string one character at a time instead of using <code>String.fromCharCode(...combined)</code> with a spread operator — spreading passes every byte as a separate function argument, which blows the call stack for entries larger than ~64KB.</p>
<p>AES-GCM is an authenticated encryption scheme. It provides not only confidentiality but also integrity — if anyone tampers with the ciphertext, decryption fails. This matters because the server could theoretically modify the encrypted blobs, and AES-GCM ensures you would detect it. Note that AES-GCM only authenticates the encrypted payload itself — the plaintext metadata fields (<code>id</code>, <code>updatedAt</code>, etc.) travel outside the encryption envelope and are not integrity-protected.</p>
<p>A fresh random IV for every encryption operation means that encrypting the same entry twice produces different ciphertext, preventing identical content from producing identical blobs across pushes.</p>
<h2>The server: what it stores, what it cannot see</h2>
<p>The <a href="https://github.com/gruberb/workledger-sync">workledger-sync server</a> is a Rust service built with <a href="https://github.com/tokio-rs/axum">Axum</a> and SQLite. Its database schema is minimal:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- The sync_id column stores the auth token, NOT the raw sync ID.</span><br><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> accounts <span class="token punctuation">(</span><br>    sync_id       <span class="token keyword">TEXT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>   <span class="token comment">-- auth token: SHA-256("auth:" + syncId)</span><br>    salt          <span class="token keyword">BLOB</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span>      <span class="token comment">-- 16 random bytes, returned to client</span><br>    created_at    <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    last_seen_at  <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    entry_count   <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span> <span class="token keyword">DEFAULT</span> <span class="token number">0</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> entries <span class="token punctuation">(</span><br>    id                <span class="token keyword">TEXT</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    sync_id           <span class="token keyword">TEXT</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span> <span class="token keyword">REFERENCES</span> accounts<span class="token punctuation">(</span>sync_id<span class="token punctuation">)</span> <span class="token keyword">ON</span> <span class="token keyword">DELETE</span> <span class="token keyword">CASCADE</span><span class="token punctuation">,</span><br>    updated_at        <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    is_archived       <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span> <span class="token keyword">DEFAULT</span> <span class="token number">0</span><span class="token punctuation">,</span><br>    is_deleted        <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span> <span class="token keyword">DEFAULT</span> <span class="token number">0</span><span class="token punctuation">,</span><br>    encrypted_payload <span class="token keyword">BLOB</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    integrity_hash    <span class="token keyword">TEXT</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    server_seq        <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span><br>    <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span> <span class="token punctuation">(</span>sync_id<span class="token punctuation">,</span> id<span class="token punctuation">)</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> sync_cursors <span class="token punctuation">(</span><br>    sync_id   <span class="token keyword">TEXT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span> <span class="token keyword">REFERENCES</span> accounts<span class="token punctuation">(</span>sync_id<span class="token punctuation">)</span> <span class="token keyword">ON</span> <span class="token keyword">DELETE</span> <span class="token keyword">CASCADE</span><span class="token punctuation">,</span><br>    next_seq  <span class="token keyword">INTEGER</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span> <span class="token keyword">DEFAULT</span> <span class="token number">1</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Here is what the server knows and does not know:</p>
<p><img src="/images/server_see.png" alt="What the server can and cannot see"></p>
<p>Even the column named <code>sync_id</code> in the database is misleading — it stores the auth token, not the actual sync ID. The naming was an early decision and I have to change it to be more accurate.</p>
<h3>Account creation flow</h3>
<p>When a user taps &quot;Generate Sync ID&quot; in the app, the following sequence happens:</p>
<ol>
<li>The client generates a sync ID locally: <code>wl-a1b2c3d4e5f6a7b8c9d0</code></li>
<li>The client computes <code>SHA-256(&quot;auth:&quot; + syncId)</code> to get the auth token</li>
<li>The client sends a <code>POST /api/v1/accounts</code> with the auth token in the request body</li>
<li>The server generates 16 random bytes as the salt, stores the auth token and salt, and returns the salt to the client</li>
<li>The client computes <code>SHA-256(&quot;crypto:&quot; + syncId)</code> to get the crypto seed</li>
<li>The client runs PBKDF2 with the crypto seed and server salt to derive the AES-256-GCM key</li>
<li>Sync is ready</li>
</ol>
<p><img src="/images/device_a.png" alt="Account creation flow"></p>
<p>The server enforces rate limiting on account creation to prevent abuse. But there is no CAPTCHA, no email verification, no OAuth flow. The auth token is self-authenticating — if you can produce it, you are the account owner.</p>
<h3>Multi-device sync</h3>
<p>When device B enters the same sync ID, the flow is:</p>
<ol>
<li>The client computes the auth token from the entered sync ID</li>
<li>The client calls <code>GET /api/v1/accounts/validate</code> with the auth token as an <code>X-Auth-Token</code> header</li>
<li>The server looks up the auth token, confirms the account exists, and returns the salt</li>
<li>The client derives the same encryption key (same sync ID + same salt = same key)</li>
<li>The client performs a full bidirectional sync via <code>POST /api/v1/sync/full</code></li>
</ol>
<p>The full sync sends all local entries (encrypted) to the server and receives all server entries back. Both sides merge using last-write-wins on the <code>updatedAt</code> timestamp.</p>
<p><img src="/images/device_b.png" alt="Multi-device sync"></p>
<p>This is the crucial property: because key derivation is deterministic, any device with the same sync ID and salt produces the same encryption key. The server does not coordinate key exchange. There is no key escrow. The key exists only in browser memory, derived fresh on each session.</p>
<h2>Random questions</h2>
<p>Going through this exercise, I had some random questions. So you might have them too:</p>
<h3>Is SHA-256 deterministic?</h3>
<p>Yes, absolutely. That is the entire reason this works.</p>
<p>SHA-256 is a deterministic hash function. The same input always produces the same output, on any device, in any
browser, forever. There is no randomness involved in hashing.</p>
<p>So when Device B enters <code>wl-a1b2c3d4e5f6a7b8c9d0</code>:</p>
<pre><code>Device A:  SHA-256(&quot;auth:&quot; + &quot;wl-a1b2c3d4e5f6a7b8c9d0&quot;)  →  7f3a8b...
Device B:  SHA-256(&quot;auth:&quot; + &quot;wl-a1b2c3d4e5f6a7b8c9d0&quot;)  →  7f3a8b...
                                                                ^^^^^^^^
                                                            identical every time
</code></pre>
<p>Same input, same output. Device B sends 7f3a8b... as the X-Auth-Token header, the server looks it up, finds the
account, returns the salt. Then:</p>
<pre><code>Device A:  SHA-256(&quot;crypto:&quot; + &quot;wl-a1b2c3d4e5f6a7b8c9d0&quot;)  →  e9c1f2...
Device B:  SHA-256(&quot;crypto:&quot; + &quot;wl-a1b2c3d4e5f6a7b8c9d0&quot;)  →  e9c1f2...
                                                                ^^^^^^^^
                                                            identical every time
</code></pre>
<p>Same crypto seed + same salt (from server) → PBKDF2 is also deterministic → same AES-256-GCM key on both devices.</p>
<p>The only randomness in the entire crypto pipeline is:</p>
<ol>
<li>The sync ID generation (once, on Device A)</li>
<li>The salt generation (once, on the server)</li>
<li>The IV for each encryption call (random 12 bytes every time, which is why re-encrypting the same entry produces
different ciphertext)</li>
</ol>
<p>Everything else is deterministic by design — that is what makes multi-device work without any key exchange
protocol.</p>
<h3>And why can't the server use the salt and auth token to reverse the sync ID?</h3>
<p>Because SHA-256 is a one-way function. It only works in one direction.</p>
<pre><code>&quot;auth:&quot; + &quot;wl-a1b2c3d4e5f6a7b8c9d0&quot;  →  SHA-256  →  7f3a8b...
</code></pre>
<p>There is no SHA-256-reverse function. Given 7f3a8b..., there is no mathematical operation that produces the input.
This is not encryption (which is reversible with a key) — it is hashing (which destroys information). The 256-bit
output is a fixed-size digest of arbitrary-length input. Many possible inputs map to the same output, so the
mapping is inherently irreversible.</p>
<p>The salt does not help either. The salt is used later in the pipeline, in a completely separate step:</p>
<pre><code>syncId
  │
  ├── SHA-256(&quot;auth:&quot; + syncId)   → auth token     ← server has this
  │
  └── SHA-256(&quot;crypto:&quot; + syncId) → crypto seed    ← server does NOT have this
        │
        └── PBKDF2(cryptoSeed, salt, 100k) → key   ← salt is used HERE
</code></pre>
<p>The salt participates in PBKDF2 key derivation from the crypto seed, not from the auth token. The server has the
auth token and the salt, but these two values never interact in the derivation chain. They are on separate
branches. The salt is useless without the crypto seed, and the crypto seed is a different hash that the server
never receives.</p>
<p>So the server would need to brute-force the sync ID: guess a value, hash it with <code>&quot;auth:&quot;</code> prefix, check if it
matches the stored auth token. With 80 bits of entropy (10 random bytes), that is 2⁸⁰ ≈ 1.2 × 10²⁴ guesses. At a
billion hashes per second, that takes about 38 million years.</p>
<h2>Final thoughts</h2>
<p>I am quite happy with how it turned out. In the beginning, everything really worked like magic. I could click on a &quot;generate sync ID&quot; button, it created one and started syncing immediately. I copy/pasted the same id to my other browser session, and less than a second later, I saw the same entries. This whole process made me really curious if I am ever able to implement a truly &quot;zero-knowledge&quot; sync someday.</p>
<p>And for the syncing itself: There were enough gotchas and race conditions, which I have to write another day about.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My setup for an ad-free life (featuring AdGuard Home)</title>
      <link href="https://bastiangruber.ca/posts/my-setup-for-an-ad-free-life-(featuring-adguard-home)/"/>
      <updated>2026-01-16T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/my-setup-for-an-ad-free-life-(featuring-adguard-home)/</id>
      <content type="html">
        <![CDATA[
      <h2>AdGuard Home</h2>
<p>For quite a while now I have been blocking every* ad in my home on a router level (* some ads obviously slip through, and also YouTube ads cannot be blocked on a DNS level). I remember having a very low powered virtual server running for some Linux server experimentations at the time, and I wanted to make better use of it. I knew of <a href="https://pi-hole.net/">Pi-hole</a> but this seemed <em>so much work</em> to setup.</p>
<p>This is when I found <a href="https://adguard.com/en/adguard-home/overview.html">AdGuard Home</a>. The <a href="https://adguard.com/en/adguard-linux/overview.html#instructions">HowTo install</a> is very simple and gives you a basic setup you can play around with. Why not execute 3-4 CLI commands to install the service and be done with it?</p>
<p>The code is available <a href="https://github.com/AdguardTeam/AdGuardHome">on GitHub</a>, with a very active community around it. After installing it on your server, you have to make sure that your port 53 is open and not blocked by your firewall. When you ask a device to use a DNS server, it automatically asks on port 53 unless told otherwise.</p>
<p>Very helpful gotchas:</p>
<ul>
<li>When you first install AdGuard Home on your server and have strict firewall rules, you may find DNS queries are blocked.</li>
<li>When you update your firewall rules and forget to leave port 53 open, your DNS will stop working.</li>
</ul>
<p>You can obviously change that in the settings when setting up AdGuard Home. The challenge is just with your router. It might not let you change the port for your DNS address. I know mine doesn't (Bell Aliant).</p>
<p><img src="/images/adguard_menu.png" alt="AdGuard menu"></p>
<p>AdGuard has a ton of options, the most important are the DNS blocklists. These are pre-integrated public lists which get updated frequently by different communities. You can find everything, from gambling to general ads, to social media and other NSFW websites.</p>
<p><img src="/images/adguard_blocklists.png" alt="AdGuard blocklists"></p>
<p>You can add them to your filters, and activate or deactivate them as you see fit.</p>
<p><img src="/images/adguard_filters.png" alt="AdGuard filters"></p>
<p>After you change the DNS IP address from the generic one to your own server instance where AdGuard home runs, every time you browse, the DNS query will go through AdGuard home and checks if that domain is blocked or not.</p>
<p><img src="/images/adguard_dns.png" alt="AdGuard DNS"></p>
<h2>Firefox Extensions</h2>
<p>I recommend installing <a href="https://www.firefox.com/en-US/">Firefox</a> for your online experience. After you've installed and made it your default, you can install three extensions which make the internet way more enjoyable.</p>
<p>After all the noisy ads and dangerous websites are blocked via AdGuard Home, we can go further <a href="https://bastiangruber.ca/posts/how-i-browse-the-web-in-2026/">fine-tune our online experience</a>:</p>
<ul>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/">Additional ad blocker for YouTube and Co.</a></li>
<li><a href="https://gitflic.ru/project/magnolia1234/bypass-paywalls-firefox-clean#installation">Remove paywalls</a></li>
<li><a href="https://addons.mozilla.org/en-CA/firefox/addon/consent-o-matic/">Remove Cookie notices</a></li>
</ul>
<p>The &quot;Remove paywalls&quot; is the most... let's say gray-zone extension. You should absolutely support newspapers and journalism. So if you can, don't replace your Newspaper subscription with this extension. But if you are on a tight budget - and no one can reasonably expect you pay for 10 newspaper subscriptions at the same time, use this extension to occasionally read an article you wouldn't otherwise be able to.</p>
<p>You manually have to download it from <a href="https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/?file=bypass_paywalls_clean-latest.xpi&amp;branch=main">this repository</a> and download the <code>bypass_paywalls_clean-latest.xpi</code> file. Then, you go to the Firefox settings, Extensions, click on the little gear symbol, and click &quot;Install Add-on from File...&quot;.</p>
<p><img src="/images/install_from_file.png" alt="Install from file"></p>
<h2>Gotchas</h2>
<p>With all of these extensions and setups in place, you will find that occasionally a link or a website breaks. I get school emails, and they wrap their links inside a tracker URL. I had to whitelist a few of these to participate in their communications. You can easily do this via the DNS whitelist setting in AdGuard Home. But you will find that sometimes you don't know if the website or link is broken, or your setup blocks it.</p>
<p>The other issue is that: What if my server is down? No request will get out of your home. And you might keep guessing: Is the Internet down or just my server?</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>How I browse the web in 2026</title>
      <link href="https://bastiangruber.ca/posts/how-i-browse-the-web-in-2026/"/>
      <updated>2026-01-03T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/how-i-browse-the-web-in-2026/</id>
      <content type="html">
        <![CDATA[
      <p>This blog post should serve as a time capsule for my future self. How was it like serving the Internet in 2026? What is my set up, my most visitied bookmarks, websites I frequently go to? Let's find out.</p>
<h3>Hardware excursion</h3>
<p>For the very first time, I cleanly separated my work (MacBook Pro M3 Pro) and my private digital life (Framework AMD 13&quot; with Arch + COSMIC). I deinstalled all work related apps and logins from my phone (iPhone 13 Pro Max). I use Firefox on all devices, Thunderbird on my Framework laptop (and use GMail via the browser on my work laptop). On my iPhone, I use the default Mail client. I also <a href="https://bastiangruber.ca/posts/mass-quitting-apple/">unplugged completely from Apple</a> and hope to get a <a href="https://tbot.substack.com/p/grapheneos-new-oem-partnership">GrapheneOS native Android phone soon</a>.</p>
<h3>My (Firefox) extensions</h3>
<ul>
<li><del><a href="https://addons.mozilla.org/en-US/firefox/addon/hide-youtube-shorts/">Hide shorts for Youtube</a></del> Using <a href="https://addons.mozilla.org/en-US/firefox/addon/youtube-enhancer-vc/">YouTube Enhancer</a> now for this.</li>
<li><del><a href="https://addons.mozilla.org/en-US/firefox/addon/istilldontcareaboutcookies/">I still don't care about cookies</a></del></li>
<li>Someone on lobste.rs pointed out <a href="https://addons.mozilla.org/en-US/firefox/addon/consent-o-matic/">Consent-O-Matic</a></li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/">uBlock Origin</a>
<ul>
<li>A commenter pointed out these are redundant if you use uBlock Origin (which has URL tracking protection and tracker blocking built-in):</li>
<li><del><a href="https://addons.mozilla.org/en-US/firefox/addon/sponsorblock/">SponsorBlock for YouTube - Skip Sponsorships</a></del></li>
<li><del><a href="https://addons.mozilla.org/en-US/firefox/addon/adblock-for-youtube/">AdBlocker for YouTube</a></del></li>
<li><del><a href="https://addons.mozilla.org/en-US/firefox/addon/clearurls/">ClearURLs</a></del></li>
<li><del><a href="https://addons.mozilla.org/en-US/firefox/addon/privacy-badger17/">Privacy Badger</a></del></li>
</ul>
</li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/youtube-enhancer-vc/">YouTube Enhancer</a></li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/are-na/">Are.na</a></li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/">Bitwarden Password Manager</a></li>
<li><a href="https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/?file=bypass_paywalls_clean-latest.xpi&amp;branch=main">Bypass Paywalls Clean</a></li>
</ul>
<h4>Configuring uBlock Origin for maximum coverage</h4>
<p>Open uBlock Origin settings (click the extension icon → three gear icon) and go to <strong>Filter lists</strong>. Make sure these are enabled:</p>
<p><strong>Privacy section:</strong></p>
<ul>
<li>EasyPrivacy (replaces Privacy Badger)</li>
<li>AdGuard/uBO – URL Tracking Protection (replaces ClearURLs)</li>
</ul>
<p><strong>Ads section:</strong></p>
<ul>
<li>EasyList</li>
</ul>
<p>uBlock Origin's built-in filters (enabled by default) handle YouTube ads specifically—no extra configuration needed.</p>
<p>Click &quot;Update now&quot; at the top, then &quot;Apply changes&quot;.</p>
<h3>Web Services I use</h3>
<ul>
<li><a href="https://ticktick.com">TickTick</a> for ToDos</li>
<li><a href="https://www.ynab.com/">YNAB</a> for finances</li>
<li><a href="https://excalidraw.com/">Excalidraw</a> for diagrams</li>
<li><a href="https://www.youtube.com/">YouTube</a> for...so many things honestly. I disable my <del>browser</del> watch history so no video recommendations and rabbit holes for me, perfect!</li>
<li><a href="https://miniflux.app/">Miniflux - Self hosted</a> for my own RSS needs</li>
<li><a href="https://app.wakingup.com/">WakingUp</a> for meditation</li>
<li><a href="https://github.com/gruberb">GitHub</a> for my coding projects.</li>
</ul>
<h3>Go-to websites I often browse</h3>
<ol start="0">
<li><a href="https://isthelclcpoolopen.ca/">isthelclcpoolopen.ca</a> - A dashboard I created for our local community pool.</li>
<li><a href="https://www.theverge.com/">The Verge</a></li>
<li><a href="https://www.are.na">Are.na</a></li>
<li><a href="https://pinboard.in/popular/">Popular Pinboard posts</a></li>
<li><a href="https://lobste.rs/">Lobsters</a></li>
<li><a href="https://news.ycombinator.com/">HackerNews</a></li>
<li><a href="https://www.newsminimalist.com/">NewsMinimalist</a></li>
<li><a href="https://letterboxd.com/">Letterbox</a></li>
</ol>
<h3>Flow</h3>
<p>I often times open the browser mindlessly, click on the suggested shortcuts in Firefox of my last visited websites, and see what's new. Once a day or around 3 times a week, I check my RSS feed. I really enjoy going to the websites directly, and RSS seems to... clean and stripped down. But I also don't want to check 15 blogs manually every day, so it's good enough.</p>
<p>I save websites (articles really) to my <a href="https://www.are.na/bastian-foreach-me-com/channels">Are.na account</a> and promised myself to read it later. Which... I barely do. I started a TickTick list with an every day task to read at least 2 articles from my <a href="https://www.are.na/bastian-foreach-me-com/aha-coding">&quot;Aha! Coding&quot;</a> channel. And I want to read more papers and watch tutorials this year. It's always a nice attempt, and once work is heating up, I read up on so many things I need at that moment, that I don't have much time or energy left to read &quot;general purpose&quot; content. But every time I come across a new problem, I find a saved article months later which would have helped me - at least partly - with that. So I want to make a new attempt of being religious about reading tech articles again.</p>
<p><a href="https://news.ycombinator.com/">HackerNews</a> is still one of my most visited website. I barely read the comments anymore, maybe the first one. But it's a good hub to know what's generally going on in the wider industry. Same with <a href="https://lobste.rs/">Lobsters</a>, but I enjoy going through the comments there, even of posts I am not interested in. <a href="https://pinboard.in/popular/">Pinboard</a> is another one where I go through the popular saved articles, but don't use the service myself anymore.</p>
<p>For meditation and introspection, I enjoy <a href="https://app.wakingup.com/">WakingUp</a>. It's sometimes &quot;too far out there&quot;, but it's less cluttered than the other suppose-to-be meditation apps. And I still need some prompts or guidance, especially during a hectic day with work, family, hobbies and friends.</p>
<p>For my ToDos: I actually managed to have 0 ToDo apps during most of 2025. They always stress me out seeing all the open things in my life I currently can't make progress on. Work is manged separete, and life is happening so much at the moment with kids and family, that the top 3-5 priority tasks are always obvious. But I also realized I am missing out on cleaning out my (digital) life and experimenting with project ideas or blog posts. It's good to have a place to store them again. I start to use <a href="https://ticktick.com/">TickTick</a>. I wish OmniFocus, Things and TickTick would have a baby. But TickTick is the best multi-platform tool I found, and it's &quot;good enough&quot;. I really miss going nerdy in OmniFocus, and I miss the sheer beauty and simplicity of Things. But oh well. Can't have it all it seems.</p>
<p>I mange my finances for the past 15 years with <a href="https://www.ynab.com/">YNAB</a>. It is slowly not the best tool anymore, and I want to try <a href="https://www.monarch.com/">Monarch</a>, but their Canadian bank account support doesn't seem to be working reliably. But hey, they actually have a <a href="https://www.monarch.com/connection-status">dashboard</a> for it!</p>
<p>Coding projects still live in <a href="https://github.com">GitHub</a>, and it slowly time to self host my projects. I never really collaborate much on these projects, but just want to store them somewhere where I can run workers. I would miss the hosting static websites via GitHub pages, so I have to figure out how to do that.</p>
<p>A bigger part takes <a href="https://www.youtube.com/">YouTube</a>. I listen to DJ sets, follow a few channels and often times watch tutorials or talks on it. YouTube is I think the only service which I am very happy to pay money for. Now it's owned by Google and I am sure there are some shady things going on with pushing videos front and center. I disabled my watcb history, so I see a black blank page when I open the general website. <strong>I have three YouTube bookmarks</strong>:</p>
<ul>
<li>Subscription view</li>
<li>&quot;Watch it later&quot; playlist</li>
<li>My own &quot;Music&quot; playlist</li>
</ul>
<p>So I never browse YouTube mindlessly, and get no video recommendations (disabled also via one of the YouTube extensions).</p>
<hr>
<p>That's it - that's my setup. I use <a href="https://www.firefox.com/en-CA/features/sync/">Firefox Sync</a> to have my account stored not only on my laptop, and as soon I setup Firefox somehwere else, all my extensions and bookmarks are back. And I trust Mozilla with my data (I work there and can talk to the folks and they are all great :)).</p>
<p>I don't expect that I change this setup very much in 2027 - the web feels much slower to change, but I still want to have this noted down somewhere.</p>
<div class="info">
Discussion on Lobsters: <a href="https://lobste.rs/s/awxarg/how_i_browse_web_2026">https://lobste.rs/s/awxarg/how_i_browse_web_2026</a>
</div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Mass quitting Apple</title>
      <link href="https://bastiangruber.ca/posts/mass-quitting-apple/"/>
      <updated>2025-12-22T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/mass-quitting-apple/</id>
      <content type="html">
        <![CDATA[
      <p>There was a time, probably around 2012 (13 years ago) where I was hoping, wishing Apple would do better: <a href="https://en.wikipedia.org/wiki/MobileMe">MobileMe</a>. I was in university, and was writing for the German Macworld (<a href="https://www.macwelt.de/">Macwelt</a>) at the time. Dropbox was gaining more popularity, and the phone wars were just heating up. In my mind, it would have been a dream to have a proper, built-in cloud storage so I didn't have to manually sync my iMac and MacBook Air.</p>
<p>Be careful what you wish for. MobileMe ceased to exist. It was a horrible slog to use, and the best part of this service was my shiny @me.com E-Mail address which I still have. iCloud is now fast, and every service in Apple's ecosystem is bundled under one platform. Instead of being an open, configurable environment with APIs, we have another closed ecosystem with the only goal to keep you inside.</p>
<p>In a way that makes it intentionally hard to switch services or add other devices. I started a new job almost 2 years ago, and had to give up my company MacBook, and thought to myself: I need a backup machine. I ordered the Framework laptop and put Arch + <a href="https://system76.com/cosmic">COSMIC</a> on it. I use Firefox and Thunderbird, but had no way of integrating iCloud Drive in a nice way into my backup setup.</p>
<p>Few months down the line, and COSMIC entered the beta version. It is stable and fast. I fell deeper in love with my Framework laptop, which released a 2.8k 120Hz screen (finally), which I could just order and install myself (in less than 5 minutes).</p>
<p>I also fell in love with tinkering again, and felt constrained by the whole Apple ecosystem. To a point where every device but the Apple TV seems like a drag to use these days. So, time to break up with Apple (again).</p>
<h2>The problem with bundling</h2>
<p>Here's what I realized: Apple bundles four things that should be separate.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>What it is</th>
<th>The Apple trap</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Identity</strong></td>
<td>How the world reaches you</td>
<td>Your @icloud.com address — they own it</td>
</tr>
<tr>
<td><strong>Data</strong></td>
<td>Files, photos, passwords, calendar</td>
<td>All in iCloud, all in one account</td>
</tr>
<tr>
<td><strong>Access</strong></td>
<td>How you connect securely</td>
<td>Their devices, their rules</td>
</tr>
<tr>
<td><strong>Recovery</strong></td>
<td>Getting it back when things break</td>
<td>&quot;Trust us&quot;</td>
</tr>
</tbody>
</table>
<p>One account suspension, one forgotten password, one &quot;suspicious activity&quot; flag — and all four collapse at once. Your email address, your photos, your passwords, your files. Gone.</p>
<p>I don't think Apple is going to lock me out. But I also don't want my digital life to depend on that assumption.</p>
<h2>My existing setup</h2>
<ul>
<li>I already owned a very small, dedicated server with OVH. But it was not very powerful (4GB of RAM, low storage, 100Mbit/s up/down, located in France — I live in Canada now, so slow pings). I configured NGINX, had domains pointed to the IP address, had existing SSL certificates with Let's Encrypt and other small services deployed on it.</li>
<li>I have 3-4 domains registered with GoDaddy.</li>
<li>I use it as my DNS server via <a href="https://adguard.com/en/adguard-home/overview.html">AdGuard Home</a>.</li>
</ul>
<h2>My goals</h2>
<p>I wanted a more powerful server, more RAM, bandwidth and also a faster ping. I wanted a built-in VPN, so certain services are forced to use a VPN connection or break by default. I wanted to host my own photos, files, calendars and contacts. I want an easy enough backup solution which runs by itself to a second, remote location.</p>
<h2>How I went about it</h2>
<p>The thought of setting up a whole self-hosted infrastructure seemed more than daunting. So many services to run and maintain, different installation scripts, docs which are probably outdated or don't exactly fit my particular needs etc.</p>
<p>But hey, we have a shiny new toy at our disposal: LLMs. It seemed to be perfect for this use case.</p>
<p>In my experience and usage, LLMs are vaguely helpful here. They can nail a solution, or lead you off stray if you are not careful in your planning phase. I was overwhelmed with transitioning and keeping existing files, and which services to use for calendars, contacts, photos, file sync, DNS, media handling, and legal torrenting for larger files and backups.</p>
<p>The LLM output was helpful in a way, where I could ask:</p>
<blockquote>
<p>&quot;These x,y,z are my needs. Tell me for File sync every possible self-hosting solution, and create a table with pros and cons. I want to be totally device independent, and it needs good Android, iOS, Linux and macOS apps or integration&quot;.</p>
</blockquote>
<p>This helped me get to know solutions I maybe wasn't aware of. I did this with every category to get a first overview what's out there.</p>
<p>Turns out, I knew every solution and was also aware of its pros and cons. But good to know anyway.</p>
<h2>What I chose</h2>
<table>
<thead>
<tr>
<th>Apple Service</th>
<th>Replacement</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>iCloud Keychain</td>
<td>Vaultwarden</td>
<td>Bitwarden-compatible, all the apps just work</td>
</tr>
<tr>
<td>iCloud Drive</td>
<td>Seafile</td>
<td>Fast, handles large files, decent mobile apps</td>
</tr>
<tr>
<td>Apple Photos</td>
<td>Immich</td>
<td>ML-powered search, face recognition. Impressive.</td>
</tr>
<tr>
<td>Calendar</td>
<td>Radicale</td>
<td>Simple CalDAV, native iOS/macOS support</td>
</tr>
<tr>
<td>Contacts</td>
<td>Radicale</td>
<td>CardDAV, same deal</td>
</tr>
<tr>
<td>Apple Music</td>
<td>Jellyfin</td>
<td>Media server for music and video</td>
</tr>
<tr>
<td>—</td>
<td>Transmission</td>
<td>Torrents, routed through VPN</td>
</tr>
<tr>
<td>—</td>
<td>WireGuard + Mullvad</td>
<td>VPN for specific services</td>
</tr>
<tr>
<td>—</td>
<td>AdGuard Home</td>
<td>DNS server</td>
</tr>
</tbody>
</table>
<p>For email, I went with <a href="https://migadu.com">Migadu</a>. Small Swiss company, straightforward pricing. I own the domain, they handle the mail. If they disappear, I point my MX records elsewhere and keep my address. That's the key thing — the address is mine now.</p>
<h2>How it went</h2>
<p>The real power doing this with LLMs was the guided step-by-step procedure without having to have hundreds of tabs open. I purchased a new dedicated server with OVH (this time: Intel Xeon, 64GB RAM, 4TB hard drive, located in Canada), and while I waited for the setup to be complete, I logged into my old server and printed every config to the terminal (<code>cat /path/to/config</code>) and copy pasted the whole terminal output. I pasted it in Claude and said: &quot;This is my setup, I want to re-create it on my new server.&quot;</p>
<p>It then just told me the <code>sudo apt install</code> commands and printed out the updated configs for each service, so I could just copy paste and move on. It even suggested to use a <code>tmux</code> session for the larger <code>rsync</code> operation from server A to B, so closing the <code>ssh</code> session wouldn't kill it. Obvious, but I am sure I would have closed it by accident, realized it, googled, had an &quot;Aha, of course tmux&quot; moment and looked for all the right commands on some random blog post.</p>
<blockquote>
<p>I wish LLMs would credit all their sources more. I love these blog posts, and using them subconsciously through a LLM feels just wrong.</p>
</blockquote>
<p>I really like that I can tell the LLM to guide me through it step by step, don't be overly expressive and basically just serve as a better helper so I don't have to type everything by hand.</p>
<p>Each service gets its own subdomain and NGINX acts as a reverse proxy. Setting this up was surprisingly straightforward once I understood the pattern.</p>
<h3>The NGINX reverse proxy pattern</h3>
<p>Here's what a typical service config looks like (this is for Vaultwarden):</p>
<pre class="language-nginx"><code class="language-nginx"><span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span><br>    <span class="token directive"><span class="token keyword">listen</span> <span class="token number">443</span> ssl http2</span><span class="token punctuation">;</span><br>    <span class="token directive"><span class="token keyword">server_name</span> vault.MY_DOMAIN.ca</span><span class="token punctuation">;</span><br><br>    <span class="token directive"><span class="token keyword">ssl_certificate</span> /etc/letsencrypt/live/MY_DOMAIN.ca/fullchain.pem</span><span class="token punctuation">;</span><br>    <span class="token directive"><span class="token keyword">ssl_certificate_key</span> /etc/letsencrypt/live/MY_DOMAIN.ca/privkey.pem</span><span class="token punctuation">;</span><br><br>    <span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span><br>        <span class="token directive"><span class="token keyword">proxy_pass</span> http://127.0.0.1:8080</span><span class="token punctuation">;</span><br>        <span class="token directive"><span class="token keyword">proxy_set_header</span> Host <span class="token variable">$host</span></span><span class="token punctuation">;</span><br>        <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Real-IP <span class="token variable">$remote_addr</span></span><span class="token punctuation">;</span><br>        <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-For <span class="token variable">$proxy_add_x_forwarded_for</span></span><span class="token punctuation">;</span><br>        <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-Proto <span class="token variable">$scheme</span></span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>The service (in this case, Vaultwarden) runs on <code>localhost:8080</code>. NGINX listens on 443, terminates SSL, and forwards requests. Each service follows this same pattern, just different ports and subdomains.</p>
<h3>DNS setup</h3>
<p>For each service, I add an A record in GoDaddy:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Value</th>
<th>TTL</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>vault</td>
<td><code>167.114.xxx.xxx</code></td>
<td>600</td>
</tr>
<tr>
<td>A</td>
<td>files</td>
<td><code>167.114.xxx.xxx</code></td>
<td>600</td>
</tr>
<tr>
<td>A</td>
<td>photos</td>
<td><code>167.114.xxx.xxx</code></td>
<td>600</td>
</tr>
</tbody>
</table>
<p>All subdomains point to the same server IP. NGINX looks at the <code>Host</code> header and routes to the right service. One wildcard Let's Encrypt certificate covers them all.</p>
<h3>Service highlights</h3>
<p><strong>Vaultwarden</strong> was probably the easiest. It's a single Docker container that implements the Bitwarden API. The official Bitwarden apps (iOS, Android, browser extensions) just work — you just point them to your own domain instead of <code>vault.bitwarden.com</code>.</p>
<p><strong>Seafile</strong> handles file sync. It's fast enough that I can edit a file on my desktop and see it update on my phone within seconds. The mobile apps are decent, though not as polished as iCloud Drive. The Linux client (<code>seaf-cli</code>) works great as a daemon.</p>
<p><strong>Immich</strong> is honestly mind-blowing for a self-hosted project. Face recognition, object detection, map view, all running on my own hardware. The mobile app has auto-upload and it just works. The only catch: the ML container uses about 8GB of RAM, but I have 64GB so who cares.</p>
<h2>The VPN routing trick</h2>
<p>I wanted Transmission to always go through Mullvad, but not everything else. Turns out you can do this with WireGuard and policy-based routing.</p>
<p>Transmission runs as a specific user (UID 103 on my system). I added a routing rule that sends all traffic from that UID through the WireGuard tunnel:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">ip</span> rule <span class="token function">add</span> uidrange <span class="token number">103</span>-103 lookup <span class="token number">200</span> priority <span class="token number">99</span><br><span class="token function">ip</span> route <span class="token function">add</span> default dev wg-mullvad table <span class="token number">200</span></code></pre>
<p>Everything else goes direct. I broke my connection quite a few times before I had figured it out.</p>
<h2>Backups</h2>
<p>This is the part I'm most paranoid about, and I think rightly so.</p>
<p>3-2-1 rule: three copies, two media types, one offsite.</p>
<ol>
<li><strong>Live data</strong> on the server</li>
<li><strong>Encrypted daily backup</strong> to a Hetzner Storage Box (offsite, in Germany) using <a href="https://restic.net/">restic</a></li>
<li><strong>Weekly sync</strong> to an external drive at home</li>
</ol>
<p>Daily backups run at 4am via systemd timer. Databases (Immich PostgreSQL, Seafile MariaDB) get dumped to SQL first so the snapshots are consistent.</p>
<h3>How the automation works</h3>
<p>The backup is triggered by a systemd timer that runs daily:</p>
<pre class="language-ini"><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Unit</span><span class="token punctuation">]</span></span><br><span class="token key attr-name">Description</span><span class="token punctuation">=</span><span class="token value attr-value">Daily backup to Hetzner Storage Box</span><br><span class="token key attr-name">Documentation</span><span class="token punctuation">=</span><span class="token value attr-value">https://restic.readthedocs.io/</span><br><br><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Timer</span><span class="token punctuation">]</span></span><br><span class="token key attr-name">OnCalendar</span><span class="token punctuation">=</span><span class="token value attr-value">daily</span><br><span class="token key attr-name">OnCalendar</span><span class="token punctuation">=</span><span class="token value attr-value">*-*-* 04:00:00</span><br><span class="token key attr-name">Persistent</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><br><span class="token key attr-name">RandomizedDelaySec</span><span class="token punctuation">=</span><span class="token value attr-value">15min</span><br><br><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Install</span><span class="token punctuation">]</span></span><br><span class="token key attr-name">WantedBy</span><span class="token punctuation">=</span><span class="token value attr-value">timers.target</span></code></pre>
<p>The timer calls a service that runs the backup script:</p>
<pre class="language-ini"><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Unit</span><span class="token punctuation">]</span></span><br><span class="token key attr-name">Description</span><span class="token punctuation">=</span><span class="token value attr-value">Backup to Hetzner Storage Box via Restic</span><br><span class="token key attr-name">After</span><span class="token punctuation">=</span><span class="token value attr-value">network-online.target</span><br><span class="token key attr-name">Wants</span><span class="token punctuation">=</span><span class="token value attr-value">network-online.target</span><br><br><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span><br><span class="token key attr-name">Type</span><span class="token punctuation">=</span><span class="token value attr-value">oneshot</span><br><span class="token key attr-name">ExecStart</span><span class="token punctuation">=</span><span class="token value attr-value">/usr/local/bin/maple-backup.sh</span><br><span class="token key attr-name">User</span><span class="token punctuation">=</span><span class="token value attr-value">root</span><br><span class="token key attr-name">Environment</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">RESTIC_REPOSITORY=sftp:uxxxxxx@uxxxxxx.your-storagebox.de:23/maple-backups</span>"</span><br><span class="token key attr-name">Environment</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">RESTIC_PASSWORD_FILE=/root/.restic-password</span>"</span></code></pre>
<p>The actual backup script dumps databases, then uses restic to create encrypted snapshots:</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/bin/bash</span><br><span class="token builtin class-name">set</span> <span class="token parameter variable">-euo</span> pipefail<br><br><span class="token comment"># Dump databases before backup</span><br><span class="token function">docker</span> <span class="token builtin class-name">exec</span> immich_postgres pg_dumpall <span class="token parameter variable">-U</span> postgres <span class="token operator">></span> /backup/immich-db.sql<br><span class="token function">docker</span> <span class="token builtin class-name">exec</span> seafile-mariadb mysqldump <span class="token parameter variable">-u</span> root -p<span class="token string">"<span class="token variable">$DB_PASSWORD</span>"</span> --all-databases <span class="token operator">></span> /backup/seafile-db.sql<br><br><span class="token comment"># Run restic backup</span><br>restic backup <span class="token punctuation">\</span><br>    /opt/docker/vaultwarden <span class="token punctuation">\</span><br>    /opt/docker/seafile <span class="token punctuation">\</span><br>    /opt/docker/immich <span class="token punctuation">\</span><br>    /backup <span class="token punctuation">\</span><br>    <span class="token parameter variable">--exclude</span><span class="token operator">=</span><span class="token string">'*.tmp'</span> <span class="token punctuation">\</span><br>    --exclude-caches<br><br><span class="token comment"># Cleanup old snapshots (keep last 7 daily, 4 weekly, 6 monthly)</span><br>restic forget --keep-daily <span class="token number">7</span> --keep-weekly <span class="token number">4</span> --keep-monthly <span class="token number">6</span> <span class="token parameter variable">--prune</span><br><br><span class="token builtin class-name">echo</span> <span class="token string">"Backup completed at <span class="token variable"><span class="token variable">$(</span><span class="token function">date</span><span class="token variable">)</span></span>"</span></code></pre>
<p>The Hetzner Storage Box is accessed via SFTP. Restic handles the encryption, deduplication, and compression. If the server dies, I can restore everything from the Storage Box to a new machine.</p>
<h2>What it costs</h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>Monthly (CAD)</th>
</tr>
</thead>
<tbody>
<tr>
<td>OVH dedicated server</td>
<td>$38</td>
</tr>
<tr>
<td>Mullvad VPN</td>
<td>$8</td>
</tr>
<tr>
<td>Migadu email</td>
<td>$2</td>
</tr>
<tr>
<td>Domain</td>
<td>$2</td>
</tr>
<tr>
<td>Hetzner Storage Box</td>
<td>$6</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>~$56</strong></td>
</tr>
</tbody>
</table>
<p>Compare that to iCloud+ 2TB ($13) + 1Password ($5) + Spotify ($11) + VPN ($8) = ~$37/month.</p>
<p>So yes, I'm paying about $19/month more. But I have a dedicated server with 64GB of RAM and 4TB of storage. I can run side projects on it. Experiments. Whatever I want.</p>
<h2>What I learned</h2>
<p>The technical setup wasn't that hard. NGINX configs are basically copy-paste once you get the pattern. Docker Compose makes services reproducible. Systemd timers are rock solid.</p>
<p>The hard part was the mental shift. With Apple, everything just worked — until it didn't, and then you had no control. Now I have full control — but I'm also responsible when things break.</p>
<p>I check the backup logs every week. I monitor disk space. I keep the system updated. It's maybe 30 minutes of maintenance per month. Totally worth it for the peace of mind.</p>
<p>The biggest win? <strong>I own my identity now.</strong> My email address is on my domain. My files are on my server. My passwords are in my vault. If any single service disappears tomorrow, I can move. That's the freedom I was looking for.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Non-Negotiables</title>
      <link href="https://bastiangruber.ca/posts/non-negotiables/"/>
      <updated>2024-06-16T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/non-negotiables/</id>
      <content type="html">
        <![CDATA[
      <p>I thought about &quot;What makes a &quot;good&quot; engineer&quot; for a long time, and probably will until my death bed. In the day to day, it's sometimes hard to figure out: Did I do a good job? For the technical aspect of my day to day work, I printed own my own &quot;Non Negotiables&quot; (inspired by <a href="https://www.youtube.com/watch?v=VMp0Ai-YnMk">The Bear</a>), and put it next to my desk on the wall. I aim to follow them.</p>
<p>Here they are:</p>
<h3>Understand the problem/feature to evaluate the possible solutions</h3>
<p>Can it be efficetively solved with existing things or require new code?</p>
<h3>Aim for simplicity</h3>
<p>Think about what is necessary and sufficient to solve the problem, make interfaces and interactions as simple as they can be, don’t write “clever” or confusing code unless absolutely necessary.</p>
<h3>Aim for elegance</h3>
<p>Code should be readable and look beautiful, APIs should be small and well defined.</p>
<h3>Aim for efficiency where possible</h3>
<p>Avoid too many layers of abstraction or algorithms or structures that don’t scale.</p>
<h3>Think about clear separation of policy and mechanism</h3>
<p>Aim for simple clean mechanisms that you can layer different policies on as needed.</p>
<h3>Think about extensibility</h3>
<p>What things would your solution preclude being done in the future, try to spot leaky abstractions.</p>
<h3>Think about failure scenarios and how to test for them and recover or fail as gracefully as possible.</h3>
<h3>Use useful comments to help future maintainers</h3>
<p>Including yourself!</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Tour of a HTTP request in Rust</title>
      <link href="https://bastiangruber.ca/posts/tour-of-a-http-request-in-rust/"/>
      <updated>2024-06-16T00:00:00Z</updated>
      <id>https://bastiangruber.ca/posts/tour-of-a-http-request-in-rust/</id>
      <content type="html">
        <![CDATA[
      <div class="info">
This article is part of a chapter of <a href="https://www.manning.com/books/rust-web-development" target="_blank">Rust Web Development</a> which didn't make the cut to be in the book.
</div>
<hr>
<h4 class="tldr">TL;DR</h4>
<img src="https://github.com/gruberb/bastiangruber.ca/blob/main/src/images/tldr-rust.png?raw=true" />
<hr>
<p>When we talk about a web service, we, more often than not, mean deployed code which listens on a certain IP address and port and responds to HTTP messages. There are many steps involved for two parties to be able to communicate with each other. Application developers are mainly confronted with two pieces of this process: TCP and HTTP.</p>
<p>TCP is a protocol which two parties use to establish a connection. They follow a certain pattern (three-way-handshake) where they send and receive short messages to negotiate the connection details. After establishing this connection, you can receive and send HTTP messages to the other party. HTTP is a stateless protocol which demands a response for every request, and defines, besides other options, the size and format of the data it is sending.</p>
<p>There is a great advantage in knowing how exactly the communication with TCP works and how and where HTTP is added. Having this information can help you improve the performance of your server application and make it more secure. If, for some reason, you want to choose any other protocol than TCP or HTTP, you know where and how to replace it.</p>
<div class="sidecar">
<h5>TCP vs. UDP</h5>
<p>TCP is a connection-oriented protocol which starts creating a connection via a so-called three-way-handshake. It makes sure to send packets in the right order and tries to resend them if they failed to arrive at the other side. TCP headers are therefore larger (20 bytes) and the whole process slower than if you would use UDP.</p>
<p>UDP packet headers are smaller (8 bytes) and there is no formal connection creation included in the protocol. The order of the packets is not secured and if one message fails to arrive, there is no built-in retry.
</p>
<p>You would use UDP for application like stock quotes or streaming services, gaming servers or weather applications. You can resend data more often, and if you want to implement your own retry mechanism. Also, data is getting outdated faster and you might not care about every single packet arrive in the same order or at all. UDP can also be broadcasted to several hosts whereas TCP is always a single client-server connection.
</p>
<p>You use TCP when you want a reliable data transfer. For example, banking applications or in e-commerce, where you don’t want to lose sensitive information along the way or have to communicate the state between client and server.
</p>
</div>
<h2>Rust and the OSI model</h2>
<p>Web services are deployed on computers connected to the internet. These computers have IP addresses and open ports they listen to for new messages. Applications running on these machines are signaling interest on certain messages so they can process and answer these.</p>
<div class="sidecar">
<h5>OSI model</h5>
<p>The Open Systems Interconnection (OSI) model is a helpful tool to abstract the underlying technology involved in transmitting bytes from A to B away. Bytes sent from a server to a client go through the computer itself, over to different routers and the physical wire connecting to larger endpoints.</p>
<p>The OSI model helps to visualize the stages involved in sending the bytes, and groups the parts involved in the process in different layers. The layers described in the model are Physical (1), Data link (2), Network (3), Transport (4), Session (5), Presentation (6), Application (7).
</p>
</div>
<p>A message sent to a server has to go through multiple layers and geographically different locations for it to arrive. To get a better understanding of these different layers, the OSI model was created. It is a conceptual framework to standardize the communications.</p>
<p>As we can see in the following figure, a client can’t just send over data to a server. It needs to go through many different routers to find the right server. For it not to get lost, a user application and the kernel are adding several headers to it, so each layer of the communication process knows where to route it to.</p>
<img src="https://github.com/gruberb/bastiangruber.ca/blob/main/src/images/osi.png?raw=true" />
<p>A packet goes through these different layers, and almost all of them add an extra header on top so the next layer knows how to deal with the information. Your application adds a HTTP header on top of the data it wants to send, before the kernel adds the TCP, IP and Ethernet header.</p>
<p>The receiving server goes through the same process but in reverse. It has to dismantle each header until it can read the data inside of it.</p>
<img src="https://github.com/gruberb/bastiangruber.ca/blob/main/src/images/header.png?raw=true" />
<p>The added header sizes are standardized, so the operating system and the kernel know how many bytes they have to strip out until they can read the data. We can use this information now to get a better grasp on our data we receive.</p>
<p>In the OSI overview figure earlier, we see that the ethernet and IP header are pretty non-negotiable. But everything else is more in our control. We can choose UDP for example instead of TCP and can use our own protocol instead of using HTTP. We can, for example, have security reasons to implement our own protocol (with an own header size), so intruders who are reading our messages can’t make sense of them.</p>
<p>Where does Rust come into play? A web service has to support the following mechanism for it to be able to create connection, receiving messages and sending responses:</p>
<ul>
<li>Opening a connection to another client</li>
<li>Support the different layers (TCP and HTTP)</li>
<li>Hold connections</li>
<li>Parse receiving messages</li>
<li>Send proper HTTP messages back</li>
</ul>
<p>Many other programming languages include a rich standard library to create these HTTP servers. Rust is however a little bit different. Being a Systems Programming Language, Rust wants to be as small as possible and also functioning well on micro controllers for example who don’t always want to communicate via HTTP with their peers.</p>
<p>Therefore, Rust decided just to include a basic understanding of TCP in the standard library, and no build-in support for HTTP. The blue parts (TCP/IP) are included in the Rust standard library. If you want to create web server which supports HTTP, you have to create your own. Luckily, this is a common scenario, so the community already built some battle-tested web server implementations in the past. The crate hyper for example is widely used as a http server.</p>
<div class="sidecar">
<h5>Rust crates</h5>
<p>External libraries or packages are called “crates” in Rust. They are hosted on a website called crates.io and will be retrieved once a Rust project compiles. You can add crates to a Rust project in the Cargo.toml file, and after using the cargo build or cargo run command on the terminal, the newly added crates will be downloaded and added to your local project.</p>
</div>
<p>There are also crates for web frameworks, which include all the layers beneath them (HTTP, TCP etc.) and offer all the modern ergonomics like parsing URL query parameter, reading and returning JSON and so on.</p>
<img src="https://github.com/gruberb/bastiangruber.ca/blob/main/src/images/figure_14.png?raw=true" />
<p>This also gives you a greater choice: If you just want a minimal functioning application server without much bloat doing one thing, you can create the few functions you need by hand and have a lightweight solution afterwards.</p>
<p>If you are coming from Go, NodeJS or Java, this means a shift in perspective. You probably have to look for a library which supports your needs from the start instead of going a few more miles without thinking about help from the community.</p>
<p>In addition to HTTP, you also need to make sure the connection between client and server is secure. This is handled via TLS (Transport Layer Security), a successor of SSL. Rust also hasn’t built-in TLS support, but there exist a few packages which support you in enabling TLS in your application.</p>
<h2>Opening a connection</h2>
<p>We look at an example where a browser application is sending a HTTP request to our web service which is written in Rust. We will dive shortly into how exactly the bytes arrive at the kernel, and how our Rust application is getting the bytes delivered into the running application. Note that this is all abstracted away through libraries, but you can later on choose not to use such library and implement something via the Rust core library itself.</p>
<p>In addition, it is helpful to know or at least heart about it once how exactly the flow of bytes in a web service works, so you can spot bugs, bottlenecks and other misconfigurations later on in your running application.</p>
<p>When the client sends a HTTP request, the kernel is wrapping the data in a package with a HTTP and TCP header attached to it. It arrives on our server at the so called NIC (network interface card). The client first has to establish a TCP connection to our server. Once done, our kernel opened a socket to which is listening to this address for incoming messages.</p>
<p>If you want to dig deeper into the kernel side of networking, I highly recommend <a href="https://beej.us/guide/bgnet/html/" target="_blank">Beej's Guide to Network Programming</a>.</p>
<img src="https://github.com/gruberb/bastiangruber.ca/blob/main/src/images/kernel.png?raw=true"/>
<p>When we run a web server in Rust, we also have a socket to the operating system side where we can listen to incoming messages. The kernel’s job is to copy the data from the incoming TCP message onto our internal socket and notifies us when new data arrived.</p>
<p>In detail, the kernel reads the incoming messages and figures out which TCP connection (IP address and port) it is associated with, looks up the corresponding socket and copies the data to a receive buffer.</p>
<p>It notifies the process which is listening to new data to this socket and copies the data to a new buffer once the process is signaling interest. It copies the data from the receive buffer into the read buffer so that your server application can get the bytes out of the kernel into your program.</p>
<div class="sidecar">
<h5>Forming full messages out of a stream</h5>
<p>When a client and server connect via TCP, they send data over a physical wire in a so-called stream. This data has no clear beginning and end. Once the connection is open, you send data and the kernel decides when the buffer is full and sends data out to the client and vice-versa. To be able to tell when “a full” message arrived, we need a protocol on top of TCP to tell us about the beginning, the structure and end of a message and conversation. In most cases, this protocol is HTTP.</p>
</div>
<p>We learned earlier that Rust supports TCP right out of the box. Therefore, we can create, open and listen to a TCP socket within Rust. Once we receive a message, we can also answer back on the same socket. We can basically send any text back to the socket, we just have to be aware that the other side can interpret what we are sending.</p>
<p>Let’s open a socket, so the kernel knows where to forward incoming requests to. Each socket has to know the protocol being used (TCP in our case), the IP address and the port. In Rust, the TcpListener is handling the job for us, and we can use bind to tell the kernel the address and port we are listening to.</p>
<pre class="language-rust"><code class="language-rust"><span class="token keyword">use</span> <span class="token namespace">std<span class="token punctuation">::</span>net<span class="token punctuation">::</span></span><span class="token class-name">TcpListener</span><span class="token punctuation">;</span><br><br><span class="token keyword">fn</span> <span class="token function-definition function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">let</span> listener <span class="token operator">=</span> <span class="token class-name">TcpListener</span><span class="token punctuation">::</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token string">"127.0.0.1:8080"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>   	<span class="token keyword">for</span> stream <span class="token keyword">in</span> listener<span class="token punctuation">.</span><span class="token function">incoming</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">let</span> stream <span class="token operator">=</span> stream<span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token macro property">println!</span><span class="token punctuation">(</span><span class="token string">"stream accepted {:?}"</span><span class="token punctuation">,</span> stream<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>If you use <code>cargo run</code> to start the server, open a browser and navigate to <code>localhost:8080</code>, you see that we print something like this:</p>
<pre class="language-bash"><code class="language-bash">stream accepted TcpStream <span class="token punctuation">{</span> addr: <span class="token number">127.0</span>.0.1:8080, peer: <span class="token number">127.0</span>.0.1:56931, fd: <span class="token number">4</span> <span class="token punctuation">}</span></code></pre>
<p>This is a step in the right direction. But why don’t we see any data or HTTP headers? It’s because we receive a stream and print it on the console. We actually have to read the content from the stream.</p>
<div class="sidecar">
<h5>Possible failure when starting a server</h5>
<p>Connecting to a port and establishing a connection can fail for many reasons. Therefore both the TcpListener and the stream of the type TcpStream will return a Result<T,E>. In our example we assume everything works correctly, but in a production environment, the port you are choosing can already be busy listening to another application. Once opened, the incoming stream is actually an attempt of a connection, which can fail due to buffer limitations for example. </p>
</div>
<p>When reading from the stream like that, the baseline we expect is UTF8 encoded text. At this point, the kernel already stripped away the TCP header and all we have left is the data encapsulated in it. This can either be HTTP headers + data or some other headers attached to the data.</p>
<p>Parsing our stream content, we should see the headers and also the data in plain text, and it is on us to strip away the headers to get to the real data of the message. Headers however play an important role: They help us interpret the data we receive.</p>
<p>There are many <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers">different HTTP headers</a>, and the server is in charge to interpret them in the right way and work with them.</p>
<p>When using a web framework later on, all the details are abstracted away. However it is vital to understand the flow how information arrives at your application so later on, you can choose asynchronous strategies, your own protocol and where to look for optimizations.</p>
<h2>Adding HTTP</h2>
<p>The <code>TcpListener</code> gave us a stream, which we need to read and interpret. We have to somehow take this stream and read what’s in it. For this, we need a few components. First, we need to create a new function which takes an incoming stream and writes the bytes back to a local buffer. From there we can parse the data accordingly and send back an answer.</p>
<p>We add a helper function which does exactly that for us.</p>
<pre class="language-rust"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function-definition function">handle_stream</span><span class="token punctuation">(</span><span class="token keyword">mut</span> stream<span class="token punctuation">:</span> <span class="token class-name">TcpStream</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">let</span> <span class="token keyword">mut</span> buffer <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">;</span> <span class="token number">1024</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>    stream<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token keyword">mut</span> buffer<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token macro property">println!</span><span class="token punctuation">(</span><span class="token string">"Request: {}"</span><span class="token punctuation">,</span> <span class="token class-name">String</span><span class="token punctuation">::</span><span class="token function">from_utf8_lossy</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>buffer<span class="token punctuation">[</span><span class="token punctuation">..</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>All we have to do is to call this function in our iterator.</p>
<pre class="language-rust"><code class="language-rust"><span class="token keyword">use</span> <span class="token namespace">std<span class="token punctuation">::</span>net<span class="token punctuation">::</span></span><span class="token punctuation">{</span><span class="token class-name">TcpListener</span><span class="token punctuation">,</span> <span class="token class-name">TcpStream</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token keyword">use</span> <span class="token namespace">std<span class="token punctuation">::</span>io<span class="token punctuation">::</span>prelude<span class="token punctuation">::</span></span><span class="token operator">*</span><span class="token punctuation">;</span><br><br><span class="token keyword">fn</span> <span class="token function-definition function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">let</span> listener <span class="token operator">=</span> <span class="token class-name">TcpListener</span><span class="token punctuation">::</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token string">"127.0.0.1:8080"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token keyword">for</span> stream <span class="token keyword">in</span> listener<span class="token punctuation">.</span><span class="token function">incoming</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	    <span class="token keyword">let</span> stream <span class="token operator">=</span> stream<span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	    <span class="token function">handle_stream</span><span class="token punctuation">(</span>stream<span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>After starting the application again with cargo run, you can open a new browser window and navigate to the website <code>localhost:8080</code> and see what your application is printing onto the console.</p>
<p>It will vary with your browser of choice, but the current version of Safari will send multiple requests which look like the following:</p>
<pre class="language-bash"><code class="language-bash">GET / HTTP/1.1<br>Host: localhost:8080<br>Upgrade-Insecure-Requests: <span class="token number">1</span><br>Accept: text/html,application/xhtml+xml,application/xml<span class="token punctuation">;</span><span class="token assign-left variable">q</span><span class="token operator">=</span><span class="token number">0.9</span>,*/*<span class="token punctuation">;</span><span class="token assign-left variable">q</span><span class="token operator">=</span><span class="token number">0.8</span><br>User-Agent: Mozilla/5.0 <span class="token punctuation">(</span>Macintosh<span class="token punctuation">;</span> Intel Mac OS X 10_15_7<span class="token punctuation">)</span> AppleWebKit/605.1.15 <span class="token punctuation">(</span>KHTML, like Gecko<span class="token punctuation">)</span> Version/14.0 Safari/605.1.15<br>Accept-Language: en-us</code></pre>
<p>It includes:</p>
<ul>
<li><code>GET</code>: The HTTP method</li>
<li><code>/</code>: The Path on the server</li>
<li><code>HTTP/1.1</code>: The version of the HTTP protocol</li>
<li><code>HOST</code>: The host/domain of the server we want to request data from</li>
<li><code>Accept-Language</code>: Which human language we prefer and understand</li>
</ul>
<div class="sidecar">
<h5>Development workflow</h5>
<p>You can see based on this simple example that developing web services with Rust has a caveat. You have to stop and recompile your binary before you can test your code again. Since we have a very strict compiler, this can take sometimes longer than with other languages.</p>
<p>However, have in mind that you can install extensions for VIM or your IDE to run a code analyzer while you write it. This will highlight errors before you start an application again with cargo run. Since undefined behavior is almost impossible in Rust, you save countless hours afterwards compared to other languages</p>
</div>
<p>Instead of just printing out the stream, we can start to look at the HTTP specification, store the content in an array and iterate over it line by line, and create a HTTP struct out of it. This work is not trivial since we need to check the length of the message from the HTTP header and build the full message ourselves.</p>
<p>Thankfully there are already crates published in the Rust ecosystem which help you with this task. So, deploying a http server in production is much less work than we do here by hand.</p>
<div class="sidecar">
<h5>Why do we see a full HTTP message?</h5>
<p>We learned that bytes arrive in a stream with no clear beginning or end. The application layer protocol (HTTP) is responsible for structuring our byte stream. Why, however, are we seeing the HTTP request than as a full message with a beginning and end? Shouldn’t messages overlap or have missing information when getting pulled out of the stream?</p>
<p>We are just lucky. Since we have a simple application with just a few requests at once, the kernel buffer is just full enough to empty out and hand over the complete HTTP message. We can’t rely on that however in a production  ready application.</p>
</div>
<p>After you opened your browser and navigated to localhost:8080, you saw an error page. That’s because we don’t return an answer yet, which the HTTP protocol requests we do. To solve this problem, we can write onto the stream and send bytes back to the client.</p>
<pre class="language-rust"><code class="language-rust"><span class="token keyword">fn</span> <span class="token function-definition function">handle_stream</span><span class="token punctuation">(</span><span class="token keyword">mut</span> stream<span class="token punctuation">:</span> <span class="token class-name">TcpStream</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">let</span> <span class="token keyword">mut</span> buffer <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">;</span> <span class="token number">512</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>    stream<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token keyword">mut</span> buffer<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token macro property">println!</span><span class="token punctuation">(</span><span class="token string">"{}"</span><span class="token punctuation">,</span> <span class="token class-name">String</span><span class="token punctuation">::</span><span class="token function">from_utf8_lossy</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>buffer<span class="token punctuation">[</span><span class="token punctuation">..</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token string">"HTTP/1.1 200 OK\r\n\r\n"</span><span class="token punctuation">;</span><br>    stream<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span><span class="token function">as_bytes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    stream<span class="token punctuation">.</span><span class="token function">flush</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">unwrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>When you open your browser now and navigate to localhost:8080, you will get a blank page instead of an error. We successfully communicated via HTTP to another application in just a few lines of code.</p>
<hr>
<div class="info">
This article is part of a chapter of <a href="https://www.manning.com/books/rust-web-development" target="_blank">Rust Web Development</a> which didn't make the cut to be in the book.
</div>

    ]]>
      </content>
    </entry>
  
</feed>
