<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>hiignite Dev log</title>
    <link>https://hiignite.com/blog.html</link>
    <description>Notes from building a coaching platform for trainers: stack decisions, trade-offs, and what actually held up.</description>
    <language>en</language>
    <copyright>© 2026 hiignite.com</copyright>
    <lastBuildDate>Wed, 26 Aug 2026 09:00:00 +0000</lastBuildDate>
    <atom:link href="https://hiignite.com/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>The migration problem isn't the data</title>
      <link>https://hiignite.com/blog/the-migration-problem-isnt-the-data/</link>
      <guid isPermaLink="true">https://hiignite.com/blog/the-migration-problem-isnt-the-data/</guid>
      <pubDate>Wed, 26 Aug 2026 09:00:00 +0000</pubDate>
      <dc:creator>Kenan Balija</dc:creator>
      <description>A coach explains that the real barrier to switching online coaching platforms isn't exporting client data — it's rebuilding a library of exercise videos for uncommon movements. Plus: building an AI import wizard.</description>
      <content:encoded><![CDATA[<p>I talked to a coach this morning about what would actually stop him switching online coaching platforms.</p>

<p>I expected the answer to be data. Exporting his client records and programmes out of a system that would rather he didn't. That's a real problem, and some platforms do make leaving deliberately awkward.</p>

<p>That wasn't his answer.</p>

<h2>It's the videos</h2>

<p>His biggest hurdle is finding exercise videos. Not bench press — every stock library has bench press. It's the uncommon movements he actually programmes: handstand plate walks, handstand shoulder taps, the things that make his coaching <em>his</em> rather than generic.</p>

<p>Those aren't in the standard libraries. Rebuilding that collection on a new platform takes hours he doesn't have — and until it's rebuilt, the new platform is worse than the old one at precisely the thing he's known for.</p>

<p>Which reframes the whole migration problem for me. I'd been thinking about switching costs as a data problem. For him it's a content problem, and content is a lot harder to export than a spreadsheet of client records.</p>

<h2>So I have a decision to make</h2>

<p>I could film them myself. Unique, premium, mine — and slow. I'm also not confident I'd get the demonstration quality right for every movement, and a badly demonstrated handstand progression is worse than no video at all. I'll probably go this route once the app is making revenue and I can work with real professionals on it.</p>

<p>Or I could use free and open resources. Much faster, but it comes with real risks I'd need to work through first: licensing, attribution, inconsistent quality, and what happens when a source disappears and takes part of the library with it.</p>

<p>I'm still thinking on this one. If you've solved exercise video libraries before, I'd genuinely like to hear how.</p>

<h2>Meanwhile: the import wizard</h2>

<p>This week I'm building the part I <em>can</em> solve. Coaches screenshot their programmes from their old platform, and AI reconstructs them inside hiignite — so migration stops being a weekend of manual retyping.</p>

<p>It doesn't fix the video library. But it does mean the rest of the move is no longer a reason to stay put.</p>]]></content:encoded>
    </item>
    <item>
      <title>Getting Google out of the page load</title>
      <link>https://hiignite.com/blog/getting-google-out-of-the-page-load/</link>
      <guid isPermaLink="true">https://hiignite.com/blog/getting-google-out-of-the-page-load/</guid>
      <pubDate>Wed, 19 Aug 2026 09:00:00 +0000</pubDate>
      <dc:creator>Kenan Balija</dc:creator>
      <description>Why hiignite.com runs Altcha, self-hosted Umami and self-hosted webfonts instead of the Google equivalents — including why the font swap is the one people miss, and why it is now faster too.</description>
      <content:encoded><![CDATA[<p>Small stuff, but it adds up. <a href="https://hiignite.com/">hiignite.com</a> runs <a href="https://altcha.org/">Altcha</a> instead of Google reCAPTCHA, and self-hosted <a href="https://umami.is/">Umami</a> instead of Google Analytics. This week I moved the fonts off Google's CDN too.</p>

<p>All three do the same job as the thing they replaced. None of them send visitor data to Google.</p>

<h2>Three swaps</h2>

<p>Altcha runs its challenge locally — proof-of-work in the visitor's own browser, rather than a call out to a third party to grade whether someone looks human enough.</p>

<p>Umami runs on my own infrastructure. It still tells me which pages get read and roughly where readers are, without setting a cookie and without an analytics company sitting in the middle of it.</p>

<p>Fonts are now served from my own domain, so there's no request to <code>fonts.googleapis.com</code> handing over a visitor's IP on every page load.</p>

<h2>The font one is the easiest to miss</h2>

<p>That last one slips past people, and I think I know why: it arrives through a CSS import, not a script tag.</p>

<p>When you audit what a page loads, you look at scripts. Scripts are where tracking lives, so scripts are what you check. A stylesheet link doesn't read as a third party — it reads as styling. But a font request is still a request, with an IP attached, made by every visitor on every page, to a server that isn't yours.</p>

<h2>The performance argument flipped</h2>

<p>The old defence of Google Fonts was the shared cache: enough sites use the same CDN copy that visitors turn up with it already cached, so you get the font for free.</p>

<p>That argument is dead. Browser cache partitioning gives each site its own cache entry, so nobody arrives at your site with your font pre-cached from somebody else's. What's left of the CDN is a DNS lookup and a connection to a third-party origin that has to complete before the font can even begin downloading.</p>

<p>Self-hosting removes both. It's usually the faster option now. The tradeoff people were accepting on privacy grounds doesn't really exist any more.</p>

<h2>The actual reason</h2>

<p>Same pattern I've been following elsewhere: self-hosted mail, self-hosted infrastructure. Not because Google's tools are bad — they're mostly excellent, mostly free, and better engineered than what I'd build. It's that I'd rather not hand more of the stack to companies whose incentives aren't mine.</p>

<p>And it might turn out to be a real selling point. Trainers handle client health and fitness data: weights, measurements, progress photos, injury history. My guess is they care about where that ends up rather more than they tend to say out loud.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why I built the API in Rust when it wasn't the optimal choice</title>
      <link>https://hiignite.com/blog/why-rust-for-the-api/</link>
      <guid isPermaLink="true">https://hiignite.com/blog/why-rust-for-the-api/</guid>
      <pubDate>Wed, 05 Aug 2026 09:00:00 +0000</pubDate>
      <dc:creator>Kenan Balija</dc:creator>
      <description>An honest account of choosing Rust for hiignite's API as a solo developer: slower development, a compiler that refuses shortcuts, and why it was still the right call for learning.</description>
      <content:encoded><![CDATA[<p>I built <a href="https://hiignite.com/">hiignite</a>'s API in Rust, and I'll say up front that it wasn't the optimal choice for a new project. I want to write the actual reasoning down, because "why Rust" deserves a more honest answer than the one it usually gets.</p>

<h2>The honest reason</h2>

<p>I kept seeing it everywhere. It read as a next-generation language — the one that keeps turning up in the places I pay attention to — and I wanted to build something real with it rather than work through another tutorial. That's the origin of the decision. Not benchmarks, not a requirements matrix. Curiosity, plus a project I actually wanted to exist.</p>

<p>I think that's a more common reason than people admit, and I'd rather own it than reverse-engineer a business case after the fact.</p>

<h2>What it costs</h2>

<p>Development is a lot slower than in any language I already knew. Not slightly slower — a lot. Things I could have written on autopilot elsewhere became genuine problems to sit with.</p>

<p>And the compiler does not let you cut corners. There's no "I'll handle that case later," no leaving a value in a shape you haven't thought through. It refuses, and it refuses in detail, and you don't move until you've answered it. I have a love-hate relationship with it: it's the most irritating part of the day and the reason I trust the result.</p>

<p>On a project built in free time, that cost is real. Slower progress on a side project isn't an abstraction — it's the difference between shipping something and losing interest.</p>

<h2>The bet</h2>

<p>The thought was that it would mould me into a better programmer. Not "better at Rust" — better generally. A language that forces you to be explicit about ownership, about what can fail, about what might be absent, is a language that makes you notice how much you'd been leaving to luck everywhere else.</p>

<p>That part I already believe has paid off, and I don't think you need the project to launch to see it.</p>

<h2>Fewer production surprises? Ask me later</h2>

<p>The other thing you read constantly is that Rust means fewer surprises in production. I'd like it to be true. I genuinely don't know yet, because <strong>we're not live</strong>.</p>

<p>So I'm not going to claim it. Anyone telling you about their production Rust experience before they have production is describing a hope, not a result. Ask me again after launch and I'll have an actual answer — including if the answer is that it made no difference.</p>

<h2>Would I recommend it?</h2>

<p>For every project? No. If you're on a deadline, or validating an idea that might not survive the month, use what you know. Rust will slow you down, and being slow to find out an idea was wrong is the expensive kind of slow.</p>

<p>But if you want to learn something that will matter, and you've got a project you're already turning over in the back of your mind anyway — then yes. A big yes. Learning a language in the abstract teaches you syntax. Learning it while building something you care about teaches you the language.</p>

<p>That combination is the only reason this was a good decision. On its own, "Rust for a new solo project" isn't one.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why our TypeScript types come from Rust</title>
      <link>https://hiignite.com/blog/specta-rust-typescript-types/</link>
      <guid isPermaLink="true">https://hiignite.com/blog/specta-rust-typescript-types/</guid>
      <pubDate>Sun, 26 Jul 2026 09:00:00 +0000</pubDate>
      <dc:creator>Kenan Balija</dc:creator>
      <description>Specta generates hiignite's TypeScript types directly from the Rust structs, so the mobile app and web dashboard can't drift from what the API actually returns — and what that costs in workflow.</description>
      <content:encoded><![CDATA[<p>One of the better decisions in building <a href="https://hiignite.com/">hiignite</a> was letting the TypeScript types come from the Rust — not written alongside it, generated from it, by <a href="https://github.com/specta-rs/specta">Specta</a>.</p>

<p>I touched on this in the <a href="https://hiignite.com/blog/building-hiignite-rust-expo-monorepo/">stack overview</a>, but it deserves its own post, because it's the piece I'd keep if I had to throw the rest away.</p>

<h2>The problem it solves: drift</h2>

<p>In past projects, the mobile app and the web client would slowly drift out of sync with the API. Someone renames a field on the backend. The clients carry on referencing the old name. Nothing complains — TypeScript is perfectly happy, because the type it's checking against is one <em>a person wrote by hand</em>, not the one the API actually returns.</p>

<p>So you find out at runtime. Usually on a device. Usually after it shipped.</p>

<p>The insidious part is that hand-written client types <em>look</em> like type safety. You get the autocomplete, the red squiggles, the whole experience. What you don't get is any guarantee that the description still matches the thing being described.</p>

<h2>What Specta changes</h2>

<p>Specta generates the TypeScript straight from the Rust structs, so there's one source of truth and it's the one the API actually serialises:</p>

<pre><code>#[derive(Serialize, Type)]
pub struct Client {
    pub id: Uuid,
    pub name: String,
    pub active: bool,
}</code></pre>

<p>…becomes:</p>

<pre><code>export type Client = { id: string; name: string; active: boolean }</code></pre>

<p>The mobile app and the web dashboard both consume that generated file. Neither can drift from what the API returns, because neither holds an independent opinion about what the API returns.</p>

<p>Rename <code>name</code> to <code>full_name</code> in the Rust, regenerate, and both clients stop compiling — in exactly the places that reference it. The bug that used to surface as a blank field on somebody's phone now surfaces as a build error on my laptop, before I've pushed anything.</p>

<h2>The tradeoff: no free lunch</h2>

<p>Every time the Rust structs change, I have to rebind and regenerate. That's a real step in the workflow, and it's a step I can forget. Forget it and the generated types are stale — which puts you back in drift territory, just with a shorter fuse, because the next regeneration surfaces everything at once.</p>

<p>I won't pretend that's free. It's friction, it's mine to remember, and anyone who tells you code generation costs nothing hasn't run it on a Friday evening.</p>

<h2>Still worth it, every time</h2>

<p>Catching a type mismatch at compile time instead of in production is worth an extra step in the workflow. Every time.</p>

<p>The asymmetry is what makes it easy. Forgetting to regenerate costs me a confusing minute and a rebuild. <em>Not</em> having the generated contract costs a runtime bug on someone else's device, found by them, reported through a channel that may not exist yet, debugged without the stack trace I'd want. One of those is an inconvenience. The other is the kind of bug that erodes trust in the whole app.</p>

<p>For a solo build in evenings and weekends, that trade is not close.</p>]]></content:encoded>
    </item>
    <item>
      <title>Six months building hiignite: Rust, Expo, and one typed contract</title>
      <link>https://hiignite.com/blog/building-hiignite-rust-expo-monorepo/</link>
      <guid isPermaLink="true">https://hiignite.com/blog/building-hiignite-rust-expo-monorepo/</guid>
      <pubDate>Wed, 24 Jun 2026 09:00:00 +0000</pubDate>
      <dc:creator>Kenan Balija</dc:creator>
      <description>The stack behind hiignite: a mobile app, web dashboard and API in one monorepo — Expo, Rust with Axum, SeaORM, and the Specta-generated type contract that killed mobile/web mismatch bugs before they happened.</description>
      <content:encoded><![CDATA[<p>Six months ago I started building <a href="https://hiignite.com/">hiignite</a>, a coaching platform for personal trainers. Mobile app, web dashboard, and API — built as one connected monorepo system instead of three separate stacks. I'm still building it in my free time, so every decision had to survive a specific test: <em>will this still make sense to me after two weeks away from the code?</em></p>

<p>Here's what I picked, and why.</p>

<h2>One system, not three stacks</h2>

<p>The obvious way to build this is three repositories: an app, a dashboard, and a backend. Three CI setups, three dependency trees, three places where the definition of "a client" can quietly drift apart.</p>

<p>I went with a monorepo instead. Not for the tooling fashion — for the seam. When the app, the dashboard and the API live in one tree, a change to a data shape is one commit that touches every consumer of it, and the thing either builds or it doesn't. Working solo, in evenings, that feedback is worth more than any amount of separation-of-concerns purity. The failure mode I was designing against isn't "the code is messy." It's "I shipped a field rename on Tuesday and found out on Sunday that the app never got the memo."</p>

<h2>Expo for mobile <em>and</em> the web dashboard</h2>

<p>Coaches work from a laptop. Their clients work from a phone. That's two products with a lot of shared vocabulary — the same programs, the same sessions, the same clients — and I didn't want to write that vocabulary twice.</p>

<p>Expo covers both from one codebase. iOS, Android and the web dashboard come out of the same components, the same navigation, the same state. Where the platforms genuinely diverge they diverge deliberately, in a file I had to open on purpose, rather than by accident across two codebases that slowly stop agreeing with each other.</p>

<h2>Rust and Axum for the API</h2>

<p>This one I'll be honest about: I was learning Rust and I wanted to use it on something real. Toy projects teach you syntax. They don't teach you what the borrow checker feels like at 11pm when you're four layers into a request handler and something doesn't live long enough.</p>

<p><a href="https://github.com/tokio-rs/axum">Axum</a> turned out to be a good fit for exactly that reason — handlers are plain async functions, extractors are types, and most of what would be framework magic elsewhere is just the type system doing its job. The learning curve was real. What I got in return is an API where a whole category of "what if this is null" questions is answered at compile time rather than in production.</p>

<p>Would a solo builder ship faster in something they already know? Yes — and not by a little. Development in Rust is slower than in any language I'd already learned, and I took that trade deliberately. The typed contract further down is a big part of what I got back for it.</p>

<h2>SeaORM, because I came from Laravel</h2>

<p>Most of my background is Laravel, and the thing I missed most when I moved to Rust wasn't the syntax — it was migrations. Having a versioned, ordered, reversible history of the schema is not a nice-to-have; it's how you stay sane when the schema changes weekly.</p>

<p><a href="https://www.sea-ql.org/SeaORM/">SeaORM</a> is the option that felt most familiar coming from that world. Entities, relations, and a migration story that maps closely onto how I already thought about the database. It's not a one-to-one Eloquent port and it doesn't pretend to be, but the mental model transferred, which meant I spent my learning budget on Rust itself instead of on re-learning how to add a column.</p>

<h2>Specta: the decision that paid off most</h2>

<p>Here's the seam that would have hurt: the API is Rust, and both clients are TypeScript. Every endpoint is a place where a Rust struct becomes JSON and a TypeScript type has to agree with it. Hand-write those types on the client and you've built a bug generator — one that only fires at runtime, on a device, usually after you've shipped.</p>

<p><a href="https://github.com/specta-rs/specta">Specta</a> generates the TypeScript from the Rust. The struct is the source of truth:</p>

<pre><code>#[derive(Serialize, Type)]
pub struct Client {
    pub id: Uuid,
    pub name: String,
    pub active: bool,
}</code></pre>

<p>…and the client-side type falls out of it:</p>

<pre><code>export type Client = { id: string; name: string; active: boolean }</code></pre>

<p>Which sounds like a small convenience and isn't. Rename a field in Rust, regenerate, and the app and the dashboard both stop compiling in exactly the places that need attention. The classic mobile/web type-mismatch bug — where one client was updated and the other wasn't — stops being a bug you find and starts being a build you can't finish. It got caught before it happened, which is the only kind of catching that's actually cheap.</p>

<p>If I had to keep one decision from this whole list, it would be this one.</p>

<h2>Bruno, so the API explains itself</h2>

<p>API collections in <a href="https://www.usebruno.com/">Bruno</a>, checked into the repo alongside the code. The point is that the API is explorable without digging through handler source to work out what a request is supposed to look like.</p>

<p>Right now the person that helps most is me — future me, coming back to an endpoint I wrote in March. But it's also the difference between "here's the API" and "here's the API, and here's a folder of working requests you can fire at it." Bruno keeps those requests as plain files in git rather than trapped in someone's account, which is the part that made me pick it.</p>

<h2>And Docker, because why not</h2>

<p>Docker for the environment. Postgres and the API come up with one command, the same way on any machine I sit down at. It costs almost nothing to set up early and it's genuinely painful to retrofit later, so it went in early.</p>

<h2>Six months in</h2>

<p>The stack, in one line: <strong>Expo</strong> for mobile and the web dashboard, <strong>Rust + Axum</strong> for the API, <strong>SeaORM</strong> for the schema, <strong>Specta</strong> for the type contract between them, <strong>Bruno</strong> for exploring the API, <strong>Docker</strong> for the environment — all in one monorepo.</p>

<p>None of it is exotic. What's held up is the theme running through it: put the seams where a machine can check them. The monorepo makes a cross-cutting change one commit. Expo makes the app and the dashboard one vocabulary. Specta makes the Rust/TypeScript boundary a compile error instead of a support ticket. For someone building in their free time, the compiler is the only teammate reviewing my work — so I've tried to give it as much to review as possible.</p>

<p>Still building. More notes as things break.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
