<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>posts on krtffl.dev</title>
    <link>https://krtffl.dev/posts/</link>
    <description>Recent content in posts on krtffl.dev</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>© [krtffl](https://krtffl.dev)</copyright>
    <lastBuildDate>Thu, 16 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://krtffl.dev/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>i said i quit testing. then i shipped seven things.</title>
      <link>https://krtffl.dev/posts/i-quit-testing-then-shipped/</link>
      <pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/i-quit-testing-then-shipped/</guid>
      <description>&lt;p&gt;about a year ago i wrote a post declaring, with the serene confidence of a man who has never once been burned, that testing was a waste of time for a solo developer. write the feature, click around, push to prod, let the users find the bugs. i called it — and i am quoting myself here — &lt;a href=&#34;https://krtffl.dev/posts/testcontainers_103&#34;&gt;&amp;ldquo;testing enlightenment stage three.&amp;rdquo;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;i&amp;rsquo;d like to retract about sixty percent of that.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>verifactu in go: a sha-256 hash chain is spain&#39;s answer to invoice fraud</title>
      <link>https://krtffl.dev/posts/verifactu-hash-chain-go/</link>
      <pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/verifactu-hash-chain-go/</guid>
      <description>&lt;p&gt;for years the spanish way to skim a business was the &lt;em&gt;software de doble uso&lt;/em&gt; — a till program with a quiet second set of books. you ring up a sale, the customer walks off with their receipt, and then, later, the invoice simply&amp;hellip; isn&amp;rsquo;t there anymore. the record gets deleted, edited, renumbered. the tax office (the aeat) shows up, asks for invoice 47, and invoice 47 was never born. good luck proving a negative.&lt;/p&gt;
&lt;p&gt;spain&amp;rsquo;s answer, live and mandatory, is delightfully low-tech and i mean that as a compliment: make every invoice cryptographically point at the one before it, so deleting number 47 leaves a hole that screams.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>docker stack deploy silently dropped my command (and 3 other swarm lies)</title>
      <link>https://krtffl.dev/posts/docker-swarm-silent-drops/</link>
      <pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/docker-swarm-silent-drops/</guid>
      <description>&lt;p&gt;picture the scene: green across the board. the deploy script ran all three of its pre-flight gates, &lt;code&gt;docker compose config&lt;/code&gt; came back clean, &lt;code&gt;docker stack deploy&lt;/code&gt; converged every service, and the post-deploy verifier printed a wall of &lt;code&gt;OK&lt;/code&gt; in cheerful green. i closed the laptop.&lt;/p&gt;
&lt;p&gt;then, a day later, i went looking at postgres and found it running with the &lt;em&gt;default&lt;/em&gt; &lt;code&gt;max_connections&lt;/code&gt;, and no TLS, and none of the eighteen &lt;code&gt;-c&lt;/code&gt; tuning flags i&amp;rsquo;d very carefully written into the stack file. the flags were right there in the yaml. &lt;code&gt;docker compose config&lt;/code&gt; printed them back to me, perfectly. and the running container had never seen a single one of them.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>one schema crate, two analyzers: bilingual full-text search with embedded tantivy</title>
      <link>https://krtffl.dev/posts/tantivy-bilingual-search-rust/</link>
      <pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/tantivy-bilingual-search-rust/</guid>
      <description>&lt;p&gt;you can see the word. it&amp;rsquo;s right there on the page — &lt;code&gt;filósofos&lt;/code&gt;, third line of a plutarch translation i typed out by hand. you paste it into the search box, hit enter, and the archive tells you it has never heard of it. no error, no red box, no &amp;ldquo;did you mean.&amp;rdquo; a clean, confident, empty result set — byte-for-byte the same empty set you&amp;rsquo;d get for a word that genuinely isn&amp;rsquo;t in the corpus.&lt;/p&gt;
&lt;p&gt;the index knew the word. i had watched it index the word. the reader just asked for it in a dialect the writer never spoke.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>two databases, one binary: duckdb for reads, sqlite for state</title>
      <link>https://krtffl.dev/posts/duckdb-sqlite-one-binary/</link>
      <pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/duckdb-sqlite-one-binary/</guid>
      <description>&lt;p&gt;the tracking endpoint has exactly one job: say &lt;code&gt;202&lt;/code&gt; and get out of the way. a beacon fires on someone else&amp;rsquo;s website, my server catches it, and the visitor is already three paragraphs into an article before my response even lands. nobody is waiting on that request — the whole point of a &lt;code&gt;sendBeacon&lt;/code&gt; is that the page has already moved on. so the afternoon i read a flame graph and found the ingest handler parked &lt;em&gt;inside&lt;/em&gt; a &lt;code&gt;duckdb&lt;/code&gt; &lt;code&gt;INSERT&lt;/code&gt;, holding a request open while the disk did its thing, i felt a very specific kind of stupid. i&amp;rsquo;d taken a fire-and-forget pixel and taught it to block on fsync.&lt;/p&gt;
&lt;p&gt;fixing that is most of this post. but the fix only makes sense on top of a decision most tutorials would have talked me out of: i put two different databases in one small rust binary, on purpose.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>brute-forcing the pit window: when &#39;just try them all&#39; is the right call</title>
      <link>https://krtffl.dev/posts/pit-window-brute-force-search/</link>
      <pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/pit-window-brute-force-search/</guid>
      <description>&lt;p&gt;lights out. and this time the car has to stop.&lt;/p&gt;
&lt;p&gt;two posts ago i drew stintlab&amp;rsquo;s charts in rust on canvas2d and swore a chart was just rectangles and lines. one post ago i fit the &lt;a href=&#34;https://krtffl.dev/posts/ols-regression-collinearity-rust&#34;&gt;tire-degradation model&lt;/a&gt;
 by hand — ordinary least squares, cramer&amp;rsquo;s rule, and a singular-matrix guard that turned a collinearity bug into an honest &lt;code&gt;None&lt;/code&gt;. both of those were setup. this is the payoff: the thing that takes that model and actually makes a decision.&lt;/p&gt;
&lt;p&gt;because a degradation model, on its own, just draws a number — how much slower a tire gets, lap after lap. lovely. now what? a real race is decided in one call from the pit wall: &lt;em&gt;when do you box, and onto what rubber?&lt;/em&gt; stop too early and you throw away tire life; too late and you bleed seconds to a fresher car behind. that call wins races. i wanted to make it automatically, and the algorithm i reached for is the single dumbest one in the book: try every option and keep the best.&lt;/p&gt;
&lt;p&gt;and i&amp;rsquo;m going to argue that&amp;rsquo;s exactly right.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>a query language in ~1000 lines: lexer, parser, evaluator</title>
      <link>https://krtffl.dev/posts/query-dsl-lexer-parser-rust/</link>
      <pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/query-dsl-lexer-parser-rust/</guid>
      <description>&lt;p&gt;here&amp;rsquo;s a question that sounds trivial and isn&amp;rsquo;t: &lt;em&gt;show me every 5xx response that also mentions a timeout.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;in &lt;code&gt;grep&lt;/code&gt; that&amp;rsquo;s a fragile little dance — &lt;code&gt;grep -E &#39;status[=:] ?5[0-9]{2}&#39;&lt;/code&gt; piped into another &lt;code&gt;grep timeout&lt;/code&gt;, and now you&amp;rsquo;re matching &lt;code&gt;500&lt;/code&gt; inside a request id, &lt;code&gt;timeout&lt;/code&gt; inside a url, and you&amp;rsquo;ve quietly stopped trusting the output. what you &lt;em&gt;wanted&lt;/em&gt; was this:&lt;/p&gt;
&lt;div class=&#34;codeblock&#34;&gt;
    &lt;div class=&#34;codeblock__bar&#34;&gt;
        &lt;span class=&#34;codeblock__lang&#34;&gt;text&lt;/span&gt;
        &lt;button class=&#34;codeblock__copy&#34; type=&#34;button&#34; data-copy aria-label=&#34;copy code&#34;&gt;copy ⧉&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;status &amp;gt;= 500 AND message contains &amp;#34;timeout&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;and have the machine read &lt;code&gt;&amp;gt;=&lt;/code&gt; as &lt;em&gt;numeric&lt;/em&gt; greater-or-equal, &lt;code&gt;AND&lt;/code&gt; as boolean conjunction, and &lt;code&gt;&amp;quot;timeout&amp;quot;&lt;/code&gt; as a substring on one named field. that&amp;rsquo;s not grep. that&amp;rsquo;s a little language, and little languages need a lexer, a parser, and something to run them.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>don&#39;t let your backtest lie to you: leakage guards for a solo research codebase</title>
      <link>https://krtffl.dev/posts/backtest-leakage-guards/</link>
      <pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/backtest-leakage-guards/</guid>
      <description>&lt;p&gt;lights on the wrong thing.&lt;/p&gt;
&lt;p&gt;the backtest printed a return rate north of 1.0 — more money notionally out than in, across a full season the model had never been allowed to see. for about four seconds i believed it. then i did the only responsible thing you can do with a number that flattering when you&amp;rsquo;re the only person checking your work: i assumed i&amp;rsquo;d cheated somewhere, and went looking for where.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>clean and verified are different claims: a two-axis trust state machine in postgres</title>
      <link>https://krtffl.dev/posts/two-axis-trust-state-machine/</link>
      <pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/two-axis-trust-state-machine/</guid>
      <description>&lt;p&gt;ask a language model for a line of tacitus in spanish and it will hand you a sentence. fluent, confident, correctly punctuated. and sometimes the sentence contains a word that no translator ever wrote — a word a scanner invented, when it read the &lt;code&gt;á&lt;/code&gt; in a 1919 printing as the digit &lt;code&gt;4&lt;/code&gt;, or turned &lt;em&gt;violento&lt;/em&gt; into &lt;code&gt;violen10&lt;/code&gt;, and then a spell-checker came along afterward and smoothed the wreckage into something that &lt;em&gt;looks&lt;/em&gt; like spanish.&lt;/p&gt;
&lt;p&gt;the text is clean. it is not correct. and nothing about the way it reads tells you which.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>cookieless analytics: how a rotating daily salt kills the consent banner</title>
      <link>https://krtffl.dev/posts/cookieless-analytics-rotating-salt/</link>
      <pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/cookieless-analytics-rotating-salt/</guid>
      <description>&lt;p&gt;every week i click &amp;ldquo;reject all&amp;rdquo; maybe forty times. cookie wall, reject all. cookie wall, manage preferences, uncheck the eight hundred &amp;ldquo;legitimate interest&amp;rdquo; partners, confirm. the banner that buries its reject button two clicks deep because the law says both choices must be equally easy and everyone pretends theirs is. it&amp;rsquo;s the loading screen of the modern web, and almost nobody &lt;em&gt;consents&lt;/em&gt; to anything — they just want the thing gone.&lt;/p&gt;
&lt;p&gt;here&amp;rsquo;s the part that took me embarrassingly long to internalise: most of those banners exist to prop up analytics that never needed your personal data in the first place.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>hand-rolling ols regression in rust, and the collinearity trap that ate my coefficients</title>
      <link>https://krtffl.dev/posts/ols-regression-collinearity-rust/</link>
      <pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/ols-regression-collinearity-rust/</guid>
      <description>&lt;p&gt;lights out, again.&lt;/p&gt;
&lt;p&gt;last time out i argued that a chart is just rectangles and lines, drew stintlab&amp;rsquo;s entire ui in rust on canvas2d to prove it, and promised — right at the bottom of &lt;a href=&#34;https://krtffl.dev/posts/rust-wasm-canvas-charts&#34;&gt;that post&lt;/a&gt;
 — to come back for &lt;em&gt;the degradation model itself&lt;/em&gt;: the math the pictures are pictures of. this is that post.&lt;/p&gt;
&lt;p&gt;the pictures draw a number. how much slower a tire gets, lap after lap, as it wears out. pin that number down and you can start to guess when a car should box. so i tried to fit it with a multiple linear regression i wrote by hand — no nalgebra, no ndarray, just the normal equations and a determinant — and the thing handed me back confident, well-formed garbage. it took a comment in one of my own tests to explain why.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>rust &#43; wasm canvas2d charts with zero javascript libraries</title>
      <link>https://krtffl.dev/posts/rust-wasm-canvas-charts/</link>
      <pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/rust-wasm-canvas-charts/</guid>
      <description>&lt;p&gt;lights out. one car jumps another off the line, and forty laps later a strategist on the pit wall wins the whole race with a single number: &lt;em&gt;when to box&lt;/em&gt;. i don&amp;rsquo;t have a pit wall. i have a laptop, a &lt;a href=&#34;https://openf1.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;free public api ↗&lt;/a&gt;
, and an unreasonable urge to see tire strategy as a &lt;em&gt;picture&lt;/em&gt; instead of a table of lap times.&lt;/p&gt;
&lt;p&gt;so i wanted a chart. the boring way to get one is &lt;code&gt;npm install&lt;/code&gt; something with a d3 buried in its dependency tree, feed it an array, and move on with my life. i did not do the boring way. i drew the whole thing in rust, compiled it to webassembly, and painted it onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; pixel by pixel with zero javascript charting libraries in the bundle.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>welford&#39;s algorithm and why i never store the samples</title>
      <link>https://krtffl.dev/posts/welford-online-variance-rust/</link>
      <pubDate>Mon, 13 Apr 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/welford-online-variance-rust/</guid>
      <description>&lt;p&gt;you want to know if the error rate just spiked. that&amp;rsquo;s a z-score: how many standard deviations is &lt;em&gt;right now&lt;/em&gt; away from what &lt;em&gt;normal&lt;/em&gt; looked like. simple formula, &lt;code&gt;(x - mean) / stddev&lt;/code&gt;, you learned it before you could legally drink.&lt;/p&gt;
&lt;p&gt;except a standard deviation needs a mean, and a mean needs all the numbers, and the numbers are a log file the size of a small moon. so where, exactly, are you keeping them?&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>multi-tenant isolation is a security boundary, not a query filter</title>
      <link>https://krtffl.dev/posts/multi-tenant-isolation-security-boundary/</link>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/multi-tenant-isolation-security-boundary/</guid>
      <description>&lt;p&gt;i&amp;rsquo;m building a small multi-tenant cms for local businesses — a barbershop, a physio, a bakery each get a little site they can edit themselves. one payload backend, one postgres, many tenants. the whole promise is that the bakery&amp;rsquo;s editor logs in and sees the bakery. not the barbershop. never the barbershop.&lt;/p&gt;
&lt;p&gt;so during a pre-launch hardening pass i did the thing i&amp;rsquo;ve learned to do: i stopped admiring the isolation and started attacking it. i logged in as a low-privilege editor — scoped to exactly one tenant — and sent one request to edit &lt;em&gt;my own user account&lt;/em&gt;. not someone else&amp;rsquo;s. mine. i just added a second tenant to the list of tenants i belong to.&lt;/p&gt;
&lt;p&gt;it went through. &lt;code&gt;200&lt;/code&gt;. and on the next request i could read, edit and delete another business&amp;rsquo;s pages as if they were my own.&lt;/p&gt;
&lt;p&gt;nobody broke in. i handed myself the keys through the front door, using an endpoint that exists specifically so users can edit themselves. an isolation boundary you can widen with a &lt;code&gt;PATCH&lt;/code&gt; to your own profile isn&amp;rsquo;t a boundary. it&amp;rsquo;s a default you forgot to close.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>building three mcp servers in rust with rmcp</title>
      <link>https://krtffl.dev/posts/mcp-servers-in-rust/</link>
      <pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/mcp-servers-in-rust/</guid>
      <description>&lt;p&gt;i wanted to ask an llm &amp;ldquo;is my server on fire?&amp;rdquo; without ssh-ing into the box, running &lt;code&gt;htop&lt;/code&gt;, squinting, giving up, and running &lt;code&gt;docker ps&lt;/code&gt; instead. i wanted it to just &lt;em&gt;know&lt;/em&gt; — pull the cpu number, list the containers, hit prometheus, and tell me in a sentence.&lt;/p&gt;
&lt;p&gt;the model context protocol makes that possible. you hand an llm a set of tools, it decides when to call them. fine. so i built one server for my infra. then i wanted one for spanish government open data — the boe, the tax calendar, the property registry. then a third for formula 1 telemetry, because it&amp;rsquo;s my blog and i do what i want.&lt;/p&gt;
&lt;p&gt;three servers. and i could already see the future: three near-identical binaries, three copies of the same http-client boilerplate, three subtly different cache bugs. the copy-paste tax.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>implementing the drain algorithm in rust</title>
      <link>https://krtffl.dev/posts/drain-algorithm-in-rust/</link>
      <pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/drain-algorithm-in-rust/</guid>
      <description>&lt;p&gt;picture the scene: something is on fire in production, you ssh into the box, &lt;code&gt;tail&lt;/code&gt; a log file, and you&amp;rsquo;re greeted by forty thousand lines that all look &lt;em&gt;almost&lt;/em&gt; the same but not quite. your eyes glaze over. you &lt;code&gt;grep error&lt;/code&gt;, then &lt;code&gt;grep -v&lt;/code&gt; the noise, then pipe the whole thing into &lt;code&gt;sort | uniq -c&lt;/code&gt; and pray.&lt;/p&gt;
&lt;p&gt;there&amp;rsquo;s a better way, and it&amp;rsquo;s older than you&amp;rsquo;d think.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>i didn&#39;t leave go for rust. i split my stack.</title>
      <link>https://krtffl.dev/posts/why-i-left-go-for-rust/</link>
      <pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/why-i-left-go-for-rust/</guid>
      <description>&lt;p&gt;&amp;ldquo;so are you a go guy or a rust guy now?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;somebody asked me that recently and i realised i&amp;rsquo;d been dodging it for the better part of a year. the honest answer is annoying, which is why nobody likes it: i&amp;rsquo;m both. and it&amp;rsquo;s not a fence i&amp;rsquo;m sitting on, it&amp;rsquo;s a &lt;em&gt;split&lt;/em&gt; — i write go and i write rust, on purpose, for different jobs. the moment i stopped treating &amp;ldquo;which language&amp;rdquo; as a personality question and started treating it as a tooling question, the whole tired argument just dissolved.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>i audited my side project like it was about to go viral (before it was)</title>
      <link>https://krtffl.dev/posts/audit-like-it-will-go-viral/</link>
      <pubDate>Mon, 29 Dec 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/audit-like-it-will-go-viral/</guid>
      <description>&lt;p&gt;i ended the last post with a promise: a public stats page that ran three full-table scans on every request and fell over the first time i pointed real concurrency at it. this is that post — the last of three findings from the same pre-launch audit of a little pairwise-voting side project, a nougat-ranking toy where you pick between two flavours and a leaderboard falls out.&lt;/p&gt;
&lt;p&gt;the first two findings were bugs: &lt;a href=&#34;https://krtffl.dev/posts/elo-lost-update-postgres&#34;&gt;an elo race that ate 79% of every vote&lt;/a&gt;
, and &lt;a href=&#34;https://krtffl.dev/posts/x-forwarded-for-rate-limit-bypass&#34;&gt;a rate limiter a rotated header walked straight through&lt;/a&gt;
. this one is a bug too, but it&amp;rsquo;s really about the &lt;em&gt;method&lt;/em&gt; that surfaced it, because the method is the transferable part. the bug — a page doing linear work per request against a table that only grows — is one you&amp;rsquo;ve almost certainly got somewhere right now and can&amp;rsquo;t see, for the exact reason i couldn&amp;rsquo;t: on your dev box the table has forty rows and everything&amp;rsquo;s instant. the page isn&amp;rsquo;t slow. the page is a landmine with the pin still in, and the only way to find it before your users do is to go step on it yourself.&lt;/p&gt;
&lt;p&gt;so that&amp;rsquo;s the method. stand up a throwaway postgres, seed it with the amount of data you&amp;rsquo;d have &lt;em&gt;if the thing actually worked&lt;/em&gt; — if it went viral, if the launch landed — and then measure the hot paths under that load instead of guessing. the very first page i pointed it at turned out to be a denial-of-service i was hosting for free, on purpose, with a smile.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>your rate limiter trusts x-forwarded-for. mine did too, until it was measured.</title>
      <link>https://krtffl.dev/posts/x-forwarded-for-rate-limit-bypass/</link>
      <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/x-forwarded-for-rate-limit-bypass/</guid>
      <description>&lt;p&gt;last time i told you about &lt;a href=&#34;https://krtffl.dev/posts/elo-lost-update-postgres&#34;&gt;the elo race that ate 79% of every vote&lt;/a&gt;
 — the anchor finding from a pre-launch hardening pass on a little pairwise-voting side project. i promised, at the bottom of that post, that the &lt;em&gt;other&lt;/em&gt; critical finding was a vote rate-limiter that a rotated header walked straight through. this is that post.&lt;/p&gt;
&lt;p&gt;here&amp;rsquo;s the setup. the side project is a nougat-ranking toy: you get shown two flavours, you pick one, an elo rating moves, a leaderboard falls out. because the leaderboard is the entire point, the interesting attack is obvious — cast a pile of votes for your favourite and bend the ranking. so i&amp;rsquo;d put a rate limiter in front of it. two, actually. i felt responsible.&lt;/p&gt;
&lt;p&gt;then, during the audit, i sat down and &lt;em&gt;attacked&lt;/em&gt; them instead of admiring them. both fell over. one to a header i let the open internet write. one to a cookie i simply didn&amp;rsquo;t send. neither took more than a few lines of &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;a limiter you have never fired a spoofed header at is not a limiter. it&amp;rsquo;s a comment.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>the elo race that silently ate 79% of every vote</title>
      <link>https://krtffl.dev/posts/elo-lost-update-postgres/</link>
      <pubDate>Mon, 15 Dec 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/elo-lost-update-postgres/</guid>
      <description>&lt;p&gt;it&amp;rsquo;s the kind of bug that lets you sleep fine for weeks.&lt;/p&gt;
&lt;p&gt;i&amp;rsquo;d built a little nougat-ranking toy — you get shown two flavours, you pick the one you&amp;rsquo;d rather eat, and an ELO rating quietly updates in the background so a leaderboard falls out of thousands of these pairwise duels. side project, single box, htmx and go and postgres, held together with &lt;code&gt;database/sql&lt;/code&gt; and good intentions. it worked. people voted. the leaderboard moved. every vote came back with a fresh pairing and a &lt;code&gt;200&lt;/code&gt;. green across the board.&lt;/p&gt;
&lt;p&gt;then, before pushing it anywhere with real traffic, i did a proper hardening pass: stand up a throwaway postgres, seed it with a plausible &lt;em&gt;went-viral&lt;/em&gt; amount of data, and hammer every endpoint like someone who wants to break it. the very first thing i measured told me that roughly &lt;strong&gt;four out of every five votes had been quietly doing almost nothing&lt;/strong&gt; the whole time.&lt;/p&gt;
&lt;p&gt;not erroring. not failing. the vote was recorded. the rating just didn&amp;rsquo;t move the way it was supposed to.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>sqlc &#43; pgx over an orm: type-safe sql that fails at compile time</title>
      <link>https://krtffl.dev/posts/sqlc-vs-orm-in-go/</link>
      <pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/sqlc-vs-orm-in-go/</guid>
      <description>&lt;p&gt;the endpoint was slow and i couldn&amp;rsquo;t tell you which query to blame, because i hadn&amp;rsquo;t written any. that&amp;rsquo;s the deal you sign with a reflection-based orm: you describe some structs, you call &lt;code&gt;db.Preload(&amp;quot;Photos&amp;quot;).Find(&amp;amp;pets)&lt;/code&gt;, and somewhere behind the curtain the library assembles sql you never see, fires off a number of round trips you never counted, and hands you back objects. when it&amp;rsquo;s fast, it&amp;rsquo;s lovely. when it&amp;rsquo;s slow, you&amp;rsquo;re debugging a library&amp;rsquo;s imagination.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>vibe coding: the tiktok of programming</title>
      <link>https://krtffl.dev/posts/vibe-coding-tiktok-programming/</link>
      <pubDate>Fri, 21 Mar 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/vibe-coding-tiktok-programming/</guid>
      <description>&lt;p&gt;well, shit happened. let me tell you about something that occurred recently that made me realize we might have a problem with this whole vibe coding trend.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>the solo founder&#39;s guide to enterprise-grade infrastructure on a budget</title>
      <link>https://krtffl.dev/posts/solo-founder-infrastructure-part1/</link>
      <pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/solo-founder-infrastructure-part1/</guid>
      <description>&lt;p&gt;let me tell you a story about infrastructure. no, wait. don&amp;rsquo;t leave. i promise this won&amp;rsquo;t be boring.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>testcontainers 103</title>
      <link>https://krtffl.dev/posts/testcontainers_103/</link>
      <pubDate>Sat, 08 Mar 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/testcontainers_103/</guid>
      <description>&lt;p&gt;ah, so here we are. the long-awaited part three of my testcontainers saga. the finale? definitely. but not for the reasons you might think. spoiler alert: i&amp;rsquo;ve been wasting my time. and maybe you have too.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>owner-scoped rbac: enforcing &#39;you can only see your own&#39;</title>
      <link>https://krtffl.dev/posts/owner-scoped-rbac-go/</link>
      <pubDate>Mon, 23 Dec 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/owner-scoped-rbac-go/</guid>
      <description>&lt;p&gt;two users hit &lt;code&gt;GET /dojos&lt;/code&gt;. same route, same handler, same line of sql underneath. the admin gets back every dojo in the database. the dojo owner gets back exactly one — theirs. nobody wrote a second endpoint, nobody passed a flag. the query just quietly narrowed itself on the way down, and the owner never learns the other rows exist.&lt;/p&gt;
&lt;p&gt;the first time that worked in mojodojo i felt clever. &amp;ldquo;you can only see your own&amp;rdquo; is the whole ballgame in a multi-tenant app, and here it was falling out of the architecture for free. then i went looking for &lt;em&gt;where&lt;/em&gt; it happened, and the answer was: two different files, in two different layers, that don&amp;rsquo;t fully agree with each other. one of them has a hole you could drive a truck through.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>the boilerplate tax: fourteen entities, an 890-line router, and go</title>
      <link>https://krtffl.dev/posts/go-boilerplate-tax/</link>
      <pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/go-boilerplate-tax/</guid>
      <description>&lt;p&gt;there&amp;rsquo;s a file in mojodojo called &lt;code&gt;dojo.go&lt;/code&gt; that declares six sentinel errors. two of them are misspelled: &lt;code&gt;&amp;quot;coulnd&#39;t create dojo&amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;quot;dojo not foud&amp;quot;&lt;/code&gt;. i knew they were misspelled when i wrote them. and then i wrote the same six errors, with the same two typos, thirteen more times — &lt;code&gt;&amp;quot;coulnd&#39;t create belt&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;fighter not foud&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;coulnd&#39;t count fees&amp;quot;&lt;/code&gt; — because every entity got its own file, and i was copy-pasting the last one.&lt;/p&gt;
&lt;p&gt;nobody re-reads the ninth near-identical file. that&amp;rsquo;s the whole story of this post.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>shipping a single self-contained go binary</title>
      <link>https://krtffl.dev/posts/self-contained-go-binary/</link>
      <pubDate>Mon, 18 Nov 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/self-contained-go-binary/</guid>
      <description>&lt;p&gt;picture it: a fresh vps, your ssh key just accepted, a blank box that has never heard of your app. you &lt;code&gt;scp&lt;/code&gt; up a binary. then you &lt;code&gt;scp&lt;/code&gt; up a &lt;code&gt;config.yaml&lt;/code&gt;. then you remember the database is empty, so you &lt;code&gt;apt install&lt;/code&gt; the migrate cli, &lt;code&gt;scp&lt;/code&gt; up the &lt;code&gt;migrations/&lt;/code&gt; folder too, point one at the other, and pray the versions line up. four artifacts and a small ceremony, just to watch the thing print a banner and die because the config key got renamed two commits ago.&lt;/p&gt;
&lt;p&gt;i did that dance one too many times on a little learning project and thought: why does a single go binary need a goddamn support convoy to boot?&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>hexagonal architecture in go without the ceremony</title>
      <link>https://krtffl.dev/posts/hexagonal-architecture-in-go/</link>
      <pubDate>Mon, 14 Oct 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/hexagonal-architecture-in-go/</guid>
      <description>&lt;p&gt;every go web app i wrote before this one had The Struct. you know the one. it starts life innocent — three fields, a name, an id — and then someone needs to persist it, so it grows a &lt;code&gt;gorm:&amp;quot;primaryKey&amp;quot;&lt;/code&gt; tag. then it needs to go over the wire, so it grows a &lt;code&gt;json:&amp;quot;id&amp;quot;&lt;/code&gt; tag. then a &lt;code&gt;binding:&amp;quot;required&amp;quot;&lt;/code&gt; for the request validation, because why not. six months later that struct imports your orm, your web framework, and your validation library, and it has a method called &lt;code&gt;BeforeSave&lt;/code&gt; that quietly does business logic inside a database hook.&lt;/p&gt;
&lt;p&gt;that struct knows too much. it can&amp;rsquo;t be tested without a database. it can&amp;rsquo;t be reasoned about without opening four dependencies. and every time you change your database, your json api changes too, because they&amp;rsquo;re the same forty lines of code wearing three different hats.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>testcontainers 102</title>
      <link>https://krtffl.dev/posts/testcontainers_102/</link>
      <pubDate>Sun, 22 Sep 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/testcontainers_102/</guid>
      <description>&lt;p&gt;alright. so the last post was a simple introduction to testcontainers: what are they, how to set it up, how to run a couple integration tests against a database&amp;hellip; it was quite good for an introduction, but there were of course things missing.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>testcontainers 101</title>
      <link>https://krtffl.dev/posts/testcontainers_101/</link>
      <pubDate>Sun, 15 Sep 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/testcontainers_101/</guid>
      <description>&lt;p&gt;well, it was about time to start writing some code in this - supposedly - blog. time to get our hands dirty.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>the meta post</title>
      <link>https://krtffl.dev/posts/meta/</link>
      <pubDate>Fri, 13 Sep 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krtffl.dev/posts/meta/</guid>
      <description>&lt;p&gt;this is probably the post that makes the least sense of all but i still need to get some practice before i get into business and dive deep into the technicalities so here it is&lt;/p&gt;</description>
    </item>
    
  </channel>
</rss>
