<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Tutorials · Avinash Gosavi</title>
  <subtitle>Posts tagged “Tutorials” by Avinash Gosavi.</subtitle>
  <id>https://www.avinashgosavi.com/feed/topic/tutorials.xml</id>
  <link href="https://www.avinashgosavi.com/feed/topic/tutorials.xml" rel="self" type="application/atom+xml"/>
  <link href="https://www.avinashgosavi.com/blog/#tutorials" rel="alternate" type="text/html"/>
  <updated>2026-10-23T07:36:47+00:00</updated>
  <author><name>Avinash Gosavi</name><uri>https://www.avinashgosavi.com/</uri></author>
  <entry>
    <title type="html">Live Audio Transcription on a $0 Server Using Whisper</title>
    <link href="https://www.avinashgosavi.com/post/live-audio-transcription-whisper/" rel="alternate" type="text/html" title="Live Audio Transcription on a $0 Server Using Whisper"/>
    <id>https://www.avinashgosavi.com/post/live-audio-transcription-whisper/</id>
    <published>2026-04-12T00:00:00+00:00</published>
    <updated>2026-10-23T07:36:47+00:00</updated>
    <category term="Local-first"/>
    <category term="Tutorials"/>
    <summary type="html">Near real-time audio transcription on my own server with faster-whisper — no third-party APIs — and the open-source service I extracted from daylog.</summary>
    <content type="html" xml:base="https://www.avinashgosavi.com/post/live-audio-transcription-whisper/">&lt;p&gt;While building &lt;a href=&quot;https://github.com/aag1091-alt/daylog&quot;&gt;daylog&lt;/a&gt; — a private voice diary app — I needed a way to transcribe audio from a phone in near real-time, running entirely on my own server. No OpenAI API, no Google, no third-party services. Just a microphone, a chunk of audio, and text back.&lt;/p&gt;

&lt;p&gt;Here’s how I built it, what I learned, and the open-source service I extracted from it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;

&lt;p&gt;The browser’s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder&quot;&gt;MediaRecorder API&lt;/a&gt; lets you record audio in chunks. You can set a timeslice — every N milliseconds, it fires an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ondataavailable&lt;/code&gt; event with a blob of audio. Send that blob to a server, get back a transcript, show it to the user. Simple enough in theory.&lt;/p&gt;

&lt;p&gt;The tricky parts:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;WebM chunks are not standalone files.&lt;/strong&gt; The first chunk from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MediaRecorder&lt;/code&gt; contains the WebM file header. Every subsequent chunk is raw audio data with no header. If you send chunk #2 to ffmpeg on its own, it can’t decode it — it has no context for what codec, sample rate, or format to expect. I spent an embarrassing amount of time debugging silent transcriptions before I found this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Whisper is slow on CPU.&lt;/strong&gt; The original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openai-whisper&lt;/code&gt; package uses PyTorch and takes 15–30 seconds to transcribe a 5-second chunk on a shared CPU VM. That’s not a live transcription experience — that’s watching paint dry.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;First request is always slow.&lt;/strong&gt; Loading a 75MB model from disk takes 10–20 seconds. If you don’t warm it up at startup, the first user request times out.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-solutions&quot;&gt;The solutions&lt;/h2&gt;

&lt;h3 id=&quot;webm-header-problem&quot;&gt;WebM header problem&lt;/h3&gt;

&lt;p&gt;The fix: don’t send individual chunks. Send the &lt;strong&gt;full accumulated audio&lt;/strong&gt; on each event. Every transcription request becomes a valid, self-contained WebM file:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chunks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;recorder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ondataavailable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Full accumulated audio = always a valid WebM file&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;snapshot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;snapshot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;audio.webm&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/transcribe&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;transcript&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;setLiveTranscript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;transcript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// replace, don&apos;t append&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote class=&quot;diagram-text&quot;&gt;&lt;p&gt;&lt;strong&gt;Diagram: Why single WebM chunks fail and accumulated audio works&lt;/strong&gt; (&lt;a href=&quot;https://www.avinashgosavi.com/post/live-audio-transcription-whisper/&quot;&gt;see it in the post&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;MediaRecorder fires one blob every N milliseconds. Only chunk 1 carries the WebM file header; chunks 2 and 3 are raw audio. Sending chunk 2 on its own fails: ffmpeg has no header, so it can&apos;t decode it and the transcript comes back silent. The fix sends everything recorded so far on each event: chunk 1, then chunks 1 and 2, then chunks 1, 2 and 3. Each request starts with the header, so each is a valid WebM file, and each new transcript replaces the last one.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Only the first chunk carries the WebM header (shaded), so every request resends audio from the start.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;The trade-off: each request sends more data than the last. For a 60-second recording with 5-second chunks, the last request sends all 60 seconds. Acceptable for a personal tool, worth optimising for production.&lt;/p&gt;

&lt;h3 id=&quot;whisper-performance&quot;&gt;Whisper performance&lt;/h3&gt;

&lt;p&gt;The original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openai-whisper&lt;/code&gt; package runs on PyTorch. &lt;a href=&quot;https://github.com/SYSTRAN/faster-whisper&quot;&gt;faster-whisper&lt;/a&gt; uses CTranslate2 as its backend — it’s 2–4x faster on CPU and uses less memory. Combined with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tiny&lt;/code&gt; model and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int8&lt;/code&gt; quantization, it transcribes a 5-second chunk in roughly 2–3 seconds on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared-cpu-1x&lt;/code&gt; Fly.io machine.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;faster_whisper&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WhisperModel&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WhisperModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tiny&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;device&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compute_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;int8&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;segments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transcribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;audio.webm&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;beam_size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;segments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beam_size=1&lt;/code&gt; switches from beam search to greedy decoding. For short conversational audio it makes no meaningful difference to accuracy, but it’s noticeably faster.&lt;/p&gt;

&lt;h3 id=&quot;cold-start&quot;&gt;Cold start&lt;/h3&gt;

&lt;p&gt;Bake the model into the Docker image. It adds ~75MB to the image size but eliminates the per-deploy download:&lt;/p&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;python3 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;from faster_whisper import WhisperModel; WhisperModel(&apos;tiny&apos;, device=&apos;cpu&apos;, compute_type=&apos;int8&apos;)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-service&quot;&gt;The service&lt;/h2&gt;

&lt;p&gt;I extracted this into a standalone microservice: &lt;a href=&quot;https://github.com/aag1091-alt/whisper-live-transcribe&quot;&gt;whisper-live-transcribe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s a single FastAPI endpoint:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;POST /transcribe
Content-Type: multipart/form-data

file: &amp;lt;audio file&amp;gt;
→ { &quot;transcript&quot;: &quot;what you said&quot; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. No auth, no database, no state. Drop it behind your existing API or deploy it standalone.&lt;/p&gt;

&lt;p&gt;The full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.py&lt;/code&gt; is under 60 lines:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@app.post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/transcribe&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;transcribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UploadFile&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...)):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;suffix&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;audio.webm&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;suffix&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.webm&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tempfile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tmp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tmp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tmp_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tmp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_get_model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;segments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transcribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tmp_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;beam_size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;segments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tmp_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;unlink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;missing_ok&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;asyncio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_thread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;transcript&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asyncio.to_thread&lt;/code&gt; keeps Whisper’s CPU-bound work off the event loop so the server stays responsive to other requests while transcribing.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;deploying-to-flyio&quot;&gt;Deploying to Fly.io&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/aag1091-alt/whisper-live-transcribe
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;whisper-live-transcribe
fly launch &lt;span class=&quot;nt&quot;&gt;--name&lt;/span&gt; my-transcribe-service
fly deploy &lt;span class=&quot;nt&quot;&gt;--local-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Total cost: &lt;strong&gt;$0/month&lt;/strong&gt; on Fly’s free tier (1 shared-cpu-1x VM). The 1GB RAM config handles the model (~150MB for tiny + runtime overhead) with room to spare.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;performance-numbers&quot;&gt;Performance numbers&lt;/h2&gt;

&lt;p&gt;Measured on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared-cpu-1x&lt;/code&gt; with 1GB RAM:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Model&lt;/th&gt;
      &lt;th&gt;Size&lt;/th&gt;
      &lt;th&gt;Latency per 5s chunk&lt;/th&gt;
      &lt;th&gt;Notes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;tiny&lt;/td&gt;
      &lt;td&gt;75MB&lt;/td&gt;
      &lt;td&gt;~2–3s&lt;/td&gt;
      &lt;td&gt;Sweet spot for real-time UX&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;base&lt;/td&gt;
      &lt;td&gt;145MB&lt;/td&gt;
      &lt;td&gt;~5–8s&lt;/td&gt;
      &lt;td&gt;Better accuracy, noticeable lag&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;small&lt;/td&gt;
      &lt;td&gt;460MB&lt;/td&gt;
      &lt;td&gt;~15–20s&lt;/td&gt;
      &lt;td&gt;Needs 2GB RAM&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;For a live transcription experience &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tiny&lt;/code&gt; is the right call. The accuracy is good enough for clear speech — it occasionally stumbles on names or technical terms, but for conversational audio it’s reliable.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-i-ended-up-doing-instead&quot;&gt;What I ended up doing instead&lt;/h2&gt;

&lt;p&gt;After shipping this for daylog, I replaced it with the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API&quot;&gt;Web Speech API&lt;/a&gt; — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webkitSpeechRecognition&lt;/code&gt; on iOS Safari uses Apple’s on-device neural engine. Results are truly instant, word-by-word as you speak, and nothing leaves the device.&lt;/p&gt;

&lt;p&gt;But the server-side approach is still the right choice when:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;You need it to work in non-Safari browsers&lt;/li&gt;
  &lt;li&gt;Privacy requires avoiding Google’s servers (Chrome’s Web Speech API sends audio to Google)&lt;/li&gt;
  &lt;li&gt;You’re building a native app without browser APIs&lt;/li&gt;
  &lt;li&gt;You want to run a different language model or fine-tune for a specific domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The service is sitting open-source on GitHub if you want to use it. Swap in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;base&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;small&lt;/code&gt; for better accuracy, or point it at a GPU and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;large-v3&lt;/code&gt; for near-perfect transcription.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://github.com/aag1091-alt/whisper-live-transcribe&quot;&gt;whisper-live-transcribe on GitHub&lt;/a&gt; — PRs welcome.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where this fits in daylog: &lt;a href=&quot;/post/hobby-project-stack/&quot;&gt;the hobby project stack&lt;/a&gt; covers the rest of the setup, and &lt;a href=&quot;/post/daylog-security-architecture/&quot;&gt;daylog’s security architecture&lt;/a&gt; covers how entries are encrypted once they’re transcribed. For live audio sent to a hosted model instead, &lt;a href=&quot;/post/building-rena-ai-health-companion-gemini-live/&quot;&gt;Rena&lt;/a&gt; streams 16 kHz PCM over a WebSocket to a backend that passes it to Gemini Live.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">What Should You Actually Use to Build Your Next Hobby Project?</title>
    <link href="https://www.avinashgosavi.com/post/hobby-project-stack/" rel="alternate" type="text/html" title="What Should You Actually Use to Build Your Next Hobby Project?"/>
    <id>https://www.avinashgosavi.com/post/hobby-project-stack/</id>
    <published>2026-04-12T00:00:00+00:00</published>
    <updated>2026-10-25T06:37:12+00:00</updated>
    <category term="Tutorials"/>
    <category term="Infrastructure"/>
    <summary type="html">What to use for your next hobby project in 2025: the zero-cost stack I chose for daylog — Cloudflare Pages, Fly.io, Neon, Upstash — and why.</summary>
    <content type="html" xml:base="https://www.avinashgosavi.com/post/hobby-project-stack/">&lt;p&gt;I recently built &lt;strong&gt;daylog&lt;/strong&gt; — a personal diary app where you speak or type your thoughts and they get encrypted and stored privately, with AI transcription happening server-side using Whisper. The interesting technical core is &lt;a href=&quot;https://pypi.org/project/vaultmem/&quot;&gt;vaultmem&lt;/a&gt;, a Python SDK for building encrypted memory vaults for AI apps. Daylog is essentially a live demo of it.&lt;/p&gt;

&lt;p&gt;But the more interesting story — the one I keep getting asked about — is the stack. Because I built this with a hard constraint: &lt;strong&gt;it should cost me nothing to run&lt;/strong&gt;, and I should be able to deploy it from my laptop to a URL I can test on my phone in under 10 minutes.&lt;/p&gt;

&lt;p&gt;Here’s everything I learned about what’s actually available in 2025, what’s free, and what I’d pick if I were starting fresh today.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-constraints-that-shaped-every-decision&quot;&gt;The constraints that shaped every decision&lt;/h2&gt;

&lt;p&gt;Before comparing services, the constraints matter:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Zero fixed cost&lt;/strong&gt; — I’m not charging for this. It needs to run on free tiers or scale-to-zero infrastructure.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Phone-first&lt;/strong&gt; — I needed a real URL on day one, testable on mobile, installable as a PWA.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Privacy-first&lt;/strong&gt; — Audio diary. Nothing goes to third-party AI APIs. Transcription happens on my own server.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Deploy from laptop&lt;/strong&gt; — No CI/CD setup in the first week. Just push and it works.&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-architecture-at-a-glance&quot;&gt;The architecture at a glance&lt;/h2&gt;

&lt;p&gt;Before diving into each choice, here’s how all the pieces connect:&lt;/p&gt;

&lt;blockquote class=&quot;diagram-text&quot;&gt;&lt;p&gt;&lt;strong&gt;Diagram: daylog&apos;s hobby stack: how the pieces connect&lt;/strong&gt; (&lt;a href=&quot;https://www.avinashgosavi.com/post/hobby-project-stack/&quot;&gt;see it in the post&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;The phone or browser runs the Cloudflare Pages PWA and talks over HTTPS to the Rails API at daylog-rails-api.fly.dev, which handles auth, routing and proxying. Rails talks SQL to Neon Postgres, which holds users, sessions and tokens. Rails forwards requests to the vault service over Fly&apos;s internal network on port 8000, which is never public. The vault service, FastAPI plus vaultmem, handles transcription, search and storage. It takes a distributed lock in Upstash Redis for vault locks, reads and writes AES-256 .vmem files on a Fly Volume, and runs faster-whisper (tiny, int8, on CPU) and sentence-transformers for semantic search.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Only Rails faces the internet: it checks auth, then forwards to the vault service over Fly&apos;s private network.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;The key insight: the vault service is &lt;strong&gt;never exposed to the internet&lt;/strong&gt;. Rails acts as the public face — it handles auth, verifies the JWT, and only then forwards requests to the vault service over Fly’s private network. The vault service doesn’t need its own auth layer, rate limiting, or TLS. That’s what makes this setup so lean.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-stack-and-why&quot;&gt;The stack, and why&lt;/h2&gt;

&lt;h3 id=&quot;frontend-cloudflare-pages&quot;&gt;Frontend: Cloudflare Pages&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chose:&lt;/strong&gt; Cloudflare Pages
&lt;strong&gt;Also considered:&lt;/strong&gt; Vercel, Netlify, GitHub Pages&lt;/p&gt;

&lt;p&gt;Cloudflare Pages is the easy winner for hobby frontends. Free tier includes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Unlimited requests&lt;/li&gt;
  &lt;li&gt;500 builds/month&lt;/li&gt;
  &lt;li&gt;Global CDN&lt;/li&gt;
  &lt;li&gt;Custom domains with auto-SSL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the real reason I chose it over Vercel: &lt;strong&gt;Cloudflare’s ecosystem&lt;/strong&gt;. If you ever need edge functions, object storage (R2), or KV, they’re right there in the same platform. Vercel’s free tier is also generous, but it optimises for Next.js specifically — if you’re not using Next.js (I’m using Vite + React), Cloudflare has less lock-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify&lt;/strong&gt; is fine but feels like it’s slowly losing the race. Their free tier has a 100GB bandwidth cap and slower build times.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;My pick for your next hobby frontend:&lt;/strong&gt; Cloudflare Pages, unless you’re building a Next.js app — then Vercel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;backend-flyio&quot;&gt;Backend: Fly.io&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chose:&lt;/strong&gt; Fly.io
&lt;strong&gt;Also considered:&lt;/strong&gt; Railway, Render, Heroku (lol)&lt;/p&gt;

&lt;p&gt;For a backend that needs to actually run a persistent server process — not a serverless function — Fly.io is currently the best deal available:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;3 shared-cpu-1x VMs with 256MB RAM&lt;/strong&gt; completely free&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Persistent volumes&lt;/strong&gt; (I use this for encrypted vault files)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Private networking&lt;/strong&gt; between apps via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.internal&lt;/code&gt; DNS — no API keys needed between your services&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Deploy from Docker&lt;/strong&gt; — no magic, no platform-specific config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The private networking was the killer feature for daylog. My Rails API talks to the vault service at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://daylog-vault-service.internal:8000&lt;/code&gt;. Zero latency, zero egress cost, zero authentication surface area exposed to the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Railway&lt;/strong&gt; is the other serious contender. Their developer plan gives you $5 of free credit per month, which is enough to run a small app. The UX is genuinely nicer than Fly’s. But Railway doesn’t give you persistent volumes on the free tier, and their private networking story is less mature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt; has a free tier but web services sleep after 15 minutes of inactivity. If your app gets a request after sleeping, the first one takes 30+ seconds to respond. For a production app this is a dealbreaker. Their paid plans are reasonable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heroku&lt;/strong&gt; effectively killed their free tier in 2022. Don’t bother.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;My pick:&lt;/strong&gt; Fly.io if you need persistent state, private networking between services, or Docker deploys. Railway if you want a nicer UI and don’t need volumes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;database-neon-postgres&quot;&gt;Database: Neon Postgres&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chose:&lt;/strong&gt; Neon
&lt;strong&gt;Also considered:&lt;/strong&gt; Supabase, Railway Postgres, PlanetScale&lt;/p&gt;

&lt;p&gt;Neon is &lt;strong&gt;serverless Postgres&lt;/strong&gt;. It scales to zero when idle, so you’re not paying for compute when nothing’s happening. Free tier:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;512MB storage&lt;/li&gt;
  &lt;li&gt;1 project, 10 branches&lt;/li&gt;
  &lt;li&gt;Branches are perfect for testing schema changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The branch feature is genuinely underrated. You can spin up a copy of your database for a feature branch, test a migration, and delete it — all from the CLI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase&lt;/strong&gt; is the obvious alternative and it’s excellent — you get Postgres, auth, real-time, and file storage in one package. Their free tier is a 500MB Postgres database. The catch: free tier projects pause after 7 days of inactivity. Fine if you’re actively building, annoying if you step away for two weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PlanetScale&lt;/strong&gt; had the best free tier in the business for a while — then they removed it entirely in 2024. Avoid for hobby projects unless you’re paying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Railway Postgres&lt;/strong&gt; is convenient if you’re already on Railway, but pricier than Neon for the same storage.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;My pick:&lt;/strong&gt; Neon for pure SQL, Supabase if you also need auth/file storage out of the box.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;cache--ephemeral-state-upstash-redis&quot;&gt;Cache / Ephemeral State: Upstash Redis&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chose:&lt;/strong&gt; Upstash Redis
&lt;strong&gt;Also considered:&lt;/strong&gt; Railway Redis, Render Redis&lt;/p&gt;

&lt;p&gt;Upstash is serverless Redis. I use it for one thing: a distributed lock that prevents two vault service instances from opening the same encrypted vault simultaneously. Free tier:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;10,000 commands/day&lt;/li&gt;
  &lt;li&gt;256MB storage&lt;/li&gt;
  &lt;li&gt;Pay-per-request above that&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a hobby app this is effectively infinite. I’ve never come close to the limit.&lt;/p&gt;

&lt;p&gt;The key property: &lt;strong&gt;it doesn’t cost you anything when you’re not using it&lt;/strong&gt;. Railway and Render both charge you for Redis even when idle. Upstash charges zero for zero activity.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;My pick:&lt;/strong&gt; Upstash Redis for anything ephemeral, rate-limiting, locking, or session state in a hobby project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;ai--ml-whisper-on-cpu-on-your-own-server&quot;&gt;AI / ML: Whisper on CPU, on your own server&lt;/h3&gt;

&lt;p&gt;This is where daylog gets opinionated.&lt;/p&gt;

&lt;p&gt;The obvious path for audio transcription is the OpenAI Whisper API — $0.006 per minute, accurate, fast. For a diary app that’s maybe 5 minutes of audio a day, that’s $0.03/day — practically free.&lt;/p&gt;

&lt;p&gt;But I’m building a &lt;em&gt;private&lt;/em&gt; diary. Audio of your thoughts shouldn’t route through third-party APIs, even cheap ones. So I run Whisper locally on the vault service VM.&lt;/p&gt;

&lt;p&gt;The practical setup:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;faster-whisper&lt;/strong&gt; (Python, CTranslate2 backend) — 2-4x faster than the original openai-whisper package&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;tiny model with int8 quantization&lt;/strong&gt; — fits in memory, transcribes a 5-second chunk in ~2-3 seconds on a shared CPU&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Model baked into the Docker image&lt;/strong&gt; — no cold download on VM restart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: it’s slower than the API (3-5s per chunk vs instant), and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tiny&lt;/code&gt; makes occasional errors. For a private diary that’s fine. For a customer-facing product, you’d probably use the API. (The streaming setup itself is written up in &lt;a href=&quot;/post/live-audio-transcription-whisper/&quot;&gt;Live Audio Transcription on a $0 Server Using Whisper&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this teaches you:&lt;/strong&gt; The “serverless AI API” path is usually the right default for hobby projects — OpenAI, Replicate, Together AI all have generous free tiers or pay-per-use pricing that rounds to zero at hobby scale. Only run models yourself if you have a specific reason (cost at scale, privacy, customisation).&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;encrypted-storage-vaultmem&quot;&gt;Encrypted Storage: vaultmem&lt;/h3&gt;

&lt;p&gt;This is the part I want to highlight because it’s why daylog exists at all.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://pypi.org/project/vaultmem/&quot;&gt;vaultmem&lt;/a&gt; is a Python SDK that gives you an &lt;strong&gt;encrypted, embeddable memory vault&lt;/strong&gt; for AI applications. You give it a passphrase and it gives you a session where you can store text atoms, media blobs, and run semantic search — all encrypted at rest.&lt;/p&gt;

&lt;p&gt;For daylog, this means:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Your diary entries are encrypted on the server with a key derived from your passphrase&lt;/li&gt;
  &lt;li&gt;I (the server operator) can’t read your diary&lt;/li&gt;
  &lt;li&gt;The vault is a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vmem&lt;/code&gt; file on a Fly volume — no database schema, no migrations&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/post/agent-memory-glossary/#semantic-search&quot;&gt;Semantic search&lt;/a&gt; (“what did I write about three weeks ago?”) works out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK handles all the hard parts: PBKDF2 key derivation, AES-256 encryption, sentence &lt;a href=&quot;/post/agent-memory-glossary/#embedding&quot;&gt;embeddings&lt;/a&gt; for search, flush/commit semantics. You write:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;passphrase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Had a great meeting with the team today&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And you get encrypted storage with semantic search for free. (How the encryption and the encrypted search index work is in &lt;a href=&quot;/post/daylog-security-architecture/&quot;&gt;the daylog security post&lt;/a&gt;; the library itself is explained in &lt;a href=&quot;/post/vaultmem-encrypted-memory-for-ai-agents/&quot;&gt;VaultMem Part 1&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;This is what makes the whole stack work at hobby scale — instead of a Postgres schema for diary entries + a vector database for search + an encryption layer + blob storage for audio, you have one SDK and one file.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-everything-costs&quot;&gt;What everything costs&lt;/h2&gt;

&lt;p&gt;Here’s the actual bill:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Service&lt;/th&gt;
      &lt;th&gt;What I use it for&lt;/th&gt;
      &lt;th&gt;Monthly cost&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Cloudflare Pages&lt;/td&gt;
      &lt;td&gt;Web app hosting&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Fly.io&lt;/td&gt;
      &lt;td&gt;Rails API + vault service&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt; (2 VMs, free tier)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Neon&lt;/td&gt;
      &lt;td&gt;Postgres (users, sessions)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt; (free tier)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Upstash&lt;/td&gt;
      &lt;td&gt;Redis (vault locks)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt; (free tier)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The only way this crosses zero is if I add a persistent volume larger than 3GB (Fly charges $0.15/GB/month) or if my Neon database exceeds 512MB.&lt;/p&gt;

&lt;p&gt;For context: the same app on managed infrastructure — RDS, ElastiCache, EC2, CloudFront — would run $50-100/month minimum.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;my-opinionated-guide-for-your-next-hobby-project&quot;&gt;My opinionated guide for your next hobby project&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Static site / frontend only:&lt;/strong&gt;
Cloudflare Pages. Nothing else comes close on the free tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-stack app, no persistent state:&lt;/strong&gt;
Cloudflare Pages (frontend) + Cloudflare Workers (API). Everything runs at the edge, free tier is very generous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-stack app with a database:&lt;/strong&gt;
Cloudflare Pages + Fly.io + Neon. This is the setup I’d start with today. All three have genuine free tiers with no sleeping, no credit card traps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need auth out of the box:&lt;/strong&gt;
Add Supabase for the database and use their auth — or keep Neon and use Clerk (generous free tier for up to 10k MAU).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need background jobs or scheduled tasks:&lt;/strong&gt;
Fly.io handles this natively — just run a separate machine. Or use Upstash QStash ($0 for low volume).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI features:&lt;/strong&gt;
Start with the API (OpenAI, Anthropic, Together, Replicate). Only run models yourself when you have a specific reason. CPU inference on Fly is possible (I’m doing it) but it’s slow — GPU machines are still expensive even at hobby scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypted, private AI memory:&lt;/strong&gt;
vaultmem. That’s the pitch.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-one-thing-id-do-differently&quot;&gt;The one thing I’d do differently&lt;/h2&gt;

&lt;p&gt;I’d set up a simple CI/CD pipeline earlier. Deploying from the terminal (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fly deploy --local-only&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrangler pages deploy&lt;/code&gt;) is fine for the first week, but it gets old fast. GitHub Actions + Fly + Cloudflare deploy actions take maybe 30 minutes to set up and remove a lot of friction.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The broader point: the free tier ecosystem for hobby projects has genuinely never been better. In 2025 you can run a full-stack app with a real database, Redis, background jobs, and AI inference for exactly zero dollars a month — and none of these services are traps that will surprise you with a bill when a tweet goes viral.&lt;/p&gt;

&lt;p&gt;The days of “I can’t afford to run this side project” are over. The only cost now is your time.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;whats-next-for-daylog&quot;&gt;What’s next for daylog&lt;/h2&gt;

&lt;p&gt;I’m putting the finishing touches on a proper demo — the full flow of opening the app, speaking a thought, watching it transcribe in real time, and searching back through weeks of entries with a natural language query. All encrypted, all on-device passphrase, running on the stack described above.&lt;/p&gt;

&lt;p&gt;If you want to see what a privacy-first, AI-powered personal knowledge tool looks like when it costs nothing to run — keep an eye out. Demo coming soon.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;vaultmem is on PyPI. daylog is coming.&lt;/em&gt;&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title type="html">Building Your Website with Jekyll and GitHub Pages</title>
    <link href="https://www.avinashgosavi.com/post/building-your-website-with-jekyll-and-github-pages/" rel="alternate" type="text/html" title="Building Your Website with Jekyll and GitHub Pages"/>
    <id>https://www.avinashgosavi.com/post/building-your-website-with-jekyll-and-github-pages/</id>
    <published>2026-02-21T00:00:00+00:00</published>
    <updated>2026-10-25T17:36:14+00:00</updated>
    <category term="Tutorials"/>
    <category term="Web"/>
    <summary type="html">A practical guide to building a personal site with Jekyll and GitHub Pages using an AI-assisted editor: what to ask for, what to configure, where to read more.</summary>
    <content type="html" xml:base="https://www.avinashgosavi.com/post/building-your-website-with-jekyll-and-github-pages/">&lt;p&gt;A static site on Jekyll + GitHub Pages is fast, free to host, and works great with an AI-assisted editor. This post assumes you’re using something like Cursor, Codex, or Copilot to generate most of the code—so we’ll focus on &lt;strong&gt;what to ask for&lt;/strong&gt;, &lt;strong&gt;what to configure&lt;/strong&gt;, and &lt;strong&gt;where to read more&lt;/strong&gt; instead of typing everything by hand.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-you-need&quot;&gt;What You Need&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Ruby&lt;/strong&gt; (3.1+). Check with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ruby -v&lt;/code&gt;. &lt;a href=&quot;https://github.com/rbenv/rbenv&quot;&gt;rbenv&lt;/a&gt; or &lt;a href=&quot;https://rubyinstaller.org/&quot;&gt;RubyInstaller&lt;/a&gt; if you need to install.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Git&lt;/strong&gt; and a &lt;strong&gt;GitHub&lt;/strong&gt; account.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Bundler&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gem install bundler&lt;/code&gt; if you don’t have it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official intro: &lt;a href=&quot;https://jekyllrb.com/docs/&quot;&gt;Jekyll quick start&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;1-scaffold-the-site-prompts-that-work&quot;&gt;1. Scaffold the Site (Prompts That Work)&lt;/h2&gt;

&lt;p&gt;You don’t have to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jekyll new&lt;/code&gt; and then rip out the default theme. You can have your AI generate a minimal site from a short spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example prompts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“Create a minimal Jekyll 4 site: Gemfile, _config.yml, _layouts/default.html, index.md, and one blog post in _posts. Use kramdown and Rouge for code highlighting.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Add an About page and a blog listing page to my Jekyll site. Use Liquid and keep the same default layout.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you a repo with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_config.yml&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_layouts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_includes&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_posts&lt;/code&gt;, and maybe &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets/css&lt;/code&gt;. From there you can iterate with more specific prompts (e.g. “add a theme toggle”, “add a CNAME for custom domain in the GitHub Actions workflow”).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Off-the-shelf alternative:&lt;/strong&gt; Use a theme. &lt;a href=&quot;https://github.com/mmistakes/minimal-mistakes&quot;&gt;Minimal Mistakes&lt;/a&gt;, &lt;a href=&quot;https://just-the-docs.github.io/just-the-docs/&quot;&gt;Just the Docs&lt;/a&gt;, and &lt;a href=&quot;https://jekyllthemes.io/&quot;&gt;Jekyll Themes&lt;/a&gt; are good starting points. Add the theme gem to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;theme: minimal-mistakes&lt;/code&gt; (or the theme name) in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_config.yml&lt;/code&gt;. Your AI can then help you override specific layouts or add features.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;2-configuration-youll-touch&quot;&gt;2. Configuration You’ll Touch&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_config.yml&lt;/code&gt;&lt;/strong&gt; drives the site. Key bits:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Setting&lt;/th&gt;
      &lt;th&gt;What it does&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Site name and canonical URL (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://www.yoursite.com&lt;/code&gt;).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;baseurl&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Leave &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;&quot;&lt;/code&gt; if you use a &lt;strong&gt;custom domain&lt;/strong&gt; at the root. Set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;/repo-name&quot;&lt;/code&gt; only if you’re on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username.github.io/repo-name&lt;/code&gt; and want correct asset paths.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permalink&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permalink: /post/:title/&lt;/code&gt; so posts live at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/post/my-post-title/&lt;/code&gt;.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;markdown&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kramdown&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input: GFM&lt;/code&gt; is a solid default.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;highlighter&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rouge&lt;/code&gt; for syntax highlighting in code blocks.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plugins&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Common: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jekyll-sitemap&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jekyll-feed&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jekyll-seo-tag&lt;/code&gt;.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Prompt idea:&lt;/strong&gt; &lt;em&gt;“In _config.yml add jekyll-sitemap, jekyll-feed, and jekyll-seo-tag to the plugins list and set permalink to /post/:title/.”&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;3-themes-and-styling&quot;&gt;3. Themes and Styling&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Out of the box:&lt;/strong&gt; Jekyll doesn’t ship a “default theme” in the same way in Jekyll 4; you use a gem-based theme or your own CSS. Gem themes (e.g. Minimal Mistakes) give you a full look and many options; your AI can help you override a single layout or add a custom CSS variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom look:&lt;/strong&gt; If you’re building your own (like a simple blog with one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets/css/style.css&lt;/code&gt;), you can ask for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“Add CSS variables for background, text, accent, and border. Use them in the layout and in a dark/light theme class on body.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Add a theme toggle (e.g. sepia vs dark) that saves the choice in localStorage and applies a class to body.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rouge (syntax highlighting) uses classes like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.highlight .k&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.highlight .s&lt;/code&gt;—your AI can generate a small “Rouge theme” block in your CSS for light and dark backgrounds.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;4-content-and-front-matter&quot;&gt;4. Content and Front Matter&lt;/h2&gt;

&lt;p&gt;Posts go in &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_posts/&lt;/code&gt;&lt;/strong&gt; with names like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;YYYY-MM-DD-slug.md&lt;/code&gt;. Front matter drives the layout and URL:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;post&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Your&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Title&quot;&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;permalink&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/post/your-custom-slug/&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# optional; else from title/slug&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;read_time&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Prompts:&lt;/strong&gt; &lt;em&gt;“Create a new Jekyll post for [topic] with front matter: layout post, title, and read_time.”&lt;/em&gt; or &lt;em&gt;“Add an excerpt to my post front matter and show it on the blog index.”&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;5-github-pages-get-it-online&quot;&gt;5. GitHub Pages: Get It Online&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Enable Pages:&lt;/strong&gt; Repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Pages&lt;/strong&gt;. Under “Build and deployment”, choose &lt;strong&gt;GitHub Actions&lt;/strong&gt; (recommended).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build workflow:&lt;/strong&gt; You need a workflow that runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle exec jekyll build&lt;/code&gt;, then uploads &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_site&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actions/upload-pages-artifact&lt;/code&gt; and deploys with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actions/deploy-pages&lt;/code&gt;. GitHub’s own “Jekyll” template under Actions is a good start; if you use a &lt;strong&gt;custom domain at the root&lt;/strong&gt;, build &lt;strong&gt;without&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--baseurl&lt;/code&gt; so links and assets work (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run: bundle exec jekyll build&lt;/code&gt;).&lt;/p&gt;

&lt;blockquote class=&quot;diagram-text&quot;&gt;&lt;p&gt;&lt;strong&gt;Diagram: How a Jekyll site gets from a git push to your custom domain&lt;/strong&gt; (&lt;a href=&quot;https://www.avinashgosavi.com/post/building-your-website-with-jekyll-and-github-pages/&quot;&gt;see it in the post&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;A push to the repository starts a GitHub Actions workflow. It runs bundle install, then bundle exec jekyll build, without --baseurl when the site is on a custom domain at the root, which produces the _site folder. actions/upload-pages-artifact uploads _site and actions/deploy-pages publishes it to GitHub Pages at username.github.io. In your DNS, a CNAME record points www at username.github.io and A records point the apex domain at GitHub&apos;s IPs. After DNS and verification, turn on Enforce HTTPS.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Push, let the workflow build and deploy, and point your domain at Pages once.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Docs (use these instead of re-explaining everything):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site&quot;&gt;GitHub Pages: Configuring a custom domain&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/pages/getting-started-with-github-pages/securing-your-github-pages-site-with-https&quot;&gt;Securing your GitHub Pages site with HTTPS&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/troubleshooting-custom-domains-and-github-pages&quot;&gt;Troubleshooting custom domains&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Custom domain in short:&lt;/strong&gt; Add your domain in Settings → Pages. In your DNS (e.g. GoDaddy), point &lt;strong&gt;www&lt;/strong&gt; to your GitHub Pages hostname with a &lt;strong&gt;CNAME&lt;/strong&gt; (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username.github.io&lt;/code&gt;). For the apex (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yoursite.com&lt;/code&gt;), use &lt;strong&gt;A&lt;/strong&gt; records to GitHub’s IPs (see the docs). After DNS and verification, enable &lt;strong&gt;Enforce HTTPS&lt;/strong&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;6-what-to-ask-your-ai-next&quot;&gt;6. What to Ask Your AI Next&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“Add a sitemap and RSS feed to my Jekyll site using jekyll-sitemap and jekyll-feed.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Add client-side search that filters the post list by title and excerpt.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Generate a GitHub Actions workflow that builds my Jekyll site with Ruby 3.2 and deploys to GitHub Pages.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“My links on the custom domain have a double path (e.g. /repo-name/post/…). Fix the build so baseurl is empty for the production build.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Use your AI to scaffold a minimal Jekyll site, then refine with prompts about config, themes, and content. Rely on &lt;strong&gt;official Jekyll and GitHub docs&lt;/strong&gt; for deep dives; use this post as a map to &lt;em&gt;what&lt;/em&gt; to configure and &lt;em&gt;what&lt;/em&gt; to ask for so you spend less time on boilerplate and more on content and design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt; &lt;a href=&quot;https://jekyllrb.com/docs/&quot;&gt;Jekyll docs&lt;/a&gt;, &lt;a href=&quot;https://docs.github.com/en/pages&quot;&gt;GitHub Pages docs&lt;/a&gt;, &lt;a href=&quot;https://shopify.github.io/liquid/&quot;&gt;Liquid&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This site runs on the same setup — the &lt;a href=&quot;/changelog/&quot;&gt;changelog&lt;/a&gt; lists what’s changed on it lately. For a Jekyll blog that an agent writes for you, see &lt;a href=&quot;/post/ai-history-blog-generator/&quot;&gt;AI History Blog Generator&lt;/a&gt;. And if you catch yourself pasting the same instructions into your coding agent every time, &lt;a href=&quot;/post/codex-skills-baking-your-workflow-into-the-tool/&quot;&gt;Codex Skills: Baking Your Workflow Into the Tool&lt;/a&gt; covers turning them into reusable skills.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
</feed>
