<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[packagemain.tech]]></title><description><![CDATA[Hands-on, practical and real-world tutorials that you can use to build your software development skills. Main topics: Backend, Databases, Software Architecture, DevOps, Distributed Systems, APIs.]]></description><link>https://packagemain.tech</link><image><url>https://substackcdn.com/image/fetch/$s_!ya8w!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42332f2e-7531-44b1-920c-bba7831fcdbe_777x777.png</url><title>packagemain.tech</title><link>https://packagemain.tech</link></image><generator>Substack</generator><lastBuildDate>Sun, 19 Apr 2026 13:22:39 GMT</lastBuildDate><atom:link href="https://packagemain.tech/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Aliaksandr Pliutau]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[packagemain@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[packagemain@substack.com]]></itunes:email><itunes:name><![CDATA[Alex Pliutau]]></itunes:name></itunes:owner><itunes:author><![CDATA[Alex Pliutau]]></itunes:author><googleplay:owner><![CDATA[packagemain@substack.com]]></googleplay:owner><googleplay:email><![CDATA[packagemain@substack.com]]></googleplay:email><googleplay:author><![CDATA[Alex Pliutau]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Agentic pre-commit hook with Opencode Go SDK]]></title><description><![CDATA[I&#8217;ve been an avid user of Opencode for a long time, even before it became widely popular.]]></description><link>https://packagemain.tech/p/agentic-pre-commit-hook-with-opencode</link><guid isPermaLink="false">https://packagemain.tech/p/agentic-pre-commit-hook-with-opencode</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Sun, 22 Mar 2026 19:05:57 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3437df14-0de2-4965-a20a-e32ea452565e_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been an avid user of <a href="https://opencode.ai">Opencode</a> for a long time, even before it became widely popular. It&#8217;s not the only coding agent in my toolkit (I also reach for <a href="https://amp.dev">Amp</a> from time to time), but Opencode holds a special place because of its LSP integration and the dead-simple ability to swap models on the fly.</p><p>It&#8217;s also remarkably extensible. You can write plugins in TypeScript, apply custom themes, build tools and web apps around it, and even extend it with Skills. The community around it is growing fast &#8212; check out <a href="https://github.com/awesome-opencode/awesome-opencode">awesome-opencode</a> or <a href="https://opencode.cafe">opencode.cafe</a> if you want to explore what people are building.</p><p>What I recently discovered, though, is that Opencode has a <strong>Go SDK</strong> (not to be confused with <em>Opencode Go</em>), and that&#8217;s what inspired this whole project. Let&#8217;s see what we can build with it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>The Problem: Catching dumb mistakes before commit</h2><p>I&#8217;m the type of developer who absolutely hates typos in code and stray debug print statements &#8212; and also the type who produces them constantly. Regular formatters and linters don&#8217;t always catch these things, and let&#8217;s be honest: other code reviewers aren&#8217;t exactly trustworthy either. Nobody really reads the code anymore.</p><p>There are tools like CodeRabbit, GitHub Copilot, and Graphite that can review your pull requests after the fact. But what if I want to run similar checks <em>before</em> committing my code? And what if I want those checks to be configurable?</p><p>The idea: run an AI-powered code review as a <strong>pre-commit hook</strong>, using a coding agent that&#8217;s already aware of the codebase.</p><p>Since I use Opencode daily, let&#8217;s build it with that.</p><div><hr></div><h2>Opencode Server</h2><p>Here&#8217;s something a lot of people don&#8217;t realize: Opencode isn&#8217;t simply a TUI wrapper. It ships with a full server, and the TUI is just one client that talks to it. That means you can connect to the server from anywhere &#8212; your phone, a web browser, or a custom Go program.</p><p>You can start the server standalone on any port, optionally password-protected:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">opencode serve --port 4096</code></pre></div><p>This also means you can run multiple Opencode instances and agents simultaneously.</p><div><hr></div><h2>The Architecture</h2><p>Here&#8217;s the high-level flow of what we&#8217;re building:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;  git commit  &#9474;&#9472;&#9472;&#9472;&#9472;&#9654;&#9474;  pre-commit hook &#9474;&#9472;&#9472;&#9472;&#9472;&#9654;&#9474; Opencode Server&#9474;
&#9474;  (triggers)  &#9474;     &#9474;  (Go binary)     &#9474;     &#9474;                &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                              &#9474;                         &#9474;
                              &#9474;  1. Get staged diff      &#9474;
                              &#9474;  2. Create session       &#9474;
                              &#9474;  3. Send diff + prompt   &#9474;
                              &#9474;  4. Parse JSON response  &#9474;
                              &#9474;  5. Pass / Fail commit   &#9474;
                              &#9660;                         &#9660;
                     &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
                     &#9474;  Terminal Output &#9474;     &#9474;   LLM (Opus)   &#9474;
                     &#9474;  issues / pass   &#9474;     &#9474;   via Opencode &#9474;
                     &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><div><hr></div><h2>The Go SDK</h2><p>The <a href="https://github.com/anomalyco/opencode-sdk-go">Opencode Go SDK</a> wraps the standard REST API that the server exposes. For our pre-commit hook, we only need three operations:</p><p>SDK Method Purpose <code>Session.New</code> Create a fresh review session <code>Session.Prompt</code> Send the diff and get the review back <code>Session.Delete</code> Clean up the session when we&#8217;re done</p><p>That&#8217;s it. Minimal surface area for a focused tool.</p><div><hr></div><h2>Crafting the Prompt</h2><p>The pre-commit hook ships with a default prompt that asks the LLM to review for typos, unnecessary debug statements, security issues, bugs, and code style violations &#8212; essentially a lightweight CodeRabbit that runs locally.</p><p>The tricky part is that LLMs answer in an unstructured way by default, which is hard to parse programmatically. So we need to instruct the model to return its answer as strict JSON and hope it actually follows those instructions.</p><p>Here&#8217;s the full default prompt, which includes the staged git diff:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">out, err := exec.Command("git", "diff", "--cached", "--diff-algorithm=minimal").Output()
if err != nil {
    fatal("unable to get git diff: %v", err)
    return
}
diff := strings.TrimSpace(string(out))
if diff == "" {
    fmt.Println("no staged changes to review")
    return
}

prompt := `You are a code reviewer. Review the staged git diff below.

Look for typos, unnecessary debug statements, bugs, security issues, and code style problems.

Respond ONLY with a JSON object (no markdown fences, no extra text):

{
  "status":"pass|fail|warn",
  "issues": [
    {"file":"...","line":0,"severity":"error|warning|info","message":"..."}
  ]
}

If everything looks good, return {"status":"pass","issues":[]}

` + "```git diff:\n" + diff + "\n```"</code></pre></div><p>The idea is that users can later customize this prompt per-repo to match their own conventions and priorities.</p><div><hr></div><h2>Talking to Opencode</h2><p>With the prompt ready, we make a sequence of SDK calls. The flow is straightforward &#8212; create a session, send the prompt, read the response, then clean up:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">const baseURL = "http://127.0.0.1:4096"

client := opencode.NewClient(
    option.WithBaseURL(baseURL),
    option.WithMaxRetries(1),
)

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

// Create a session
session, err := client.Session.New(ctx, opencode.SessionNewParams{
    Title: opencode.F("pre-commit review"),
})
if err != nil {
    fatal("unable to create session: %v", err)
    return
}

// Always clean up
defer client.Session.Delete(context.Background(), session.ID, opencode.SessionDeleteParams{})

fmt.Fprintln(os.Stderr, "Reviewing staged changes...")

// Send the prompt
resp, err := client.Session.Prompt(ctx, session.ID, opencode.SessionPromptParams{
    Parts: opencode.F([]opencode.SessionPromptParamsPartUnion{
        opencode.TextPartInputParam{
            Type: opencode.F(opencode.TextPartInputTypeText),
            Text: opencode.F(prompt),
        },
    }),
})
if err != nil {
    fatal("unable to prompt: %v", err)
    return
}

// Extract text from response parts
var text string
for _, part := range resp.Parts {
    if tp, ok := part.AsUnion().(opencode.TextPart); ok {
        text += tp.Text
    }
}</code></pre></div><p>A one-minute timeout is more than enough for a typical diff review.</p><div><hr></div><h2>Parsing the Result</h2><p>The response comes back as a JSON string (if the model cooperated), which we deserialize into simple Go structs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type Review struct {
    Status string  `json:"status"`
    Issues []Issue `json:"issues"`
}

type Issue struct {
    File     string `json:"file"`
    Line     int    `json:"line"`
    Severity string `json:"severity"`
    Message  string `json:"message"`
}</code></pre></div><p>Then we parse and display the results:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">var review Review
if err := json.Unmarshal([]byte(text), &amp;review); err != nil {
    fatal("unable to parse json: %v\nraw response:\n%s", err, text)
    return
}

fmt.Printf("Review status: %s\n", review.Status)
for _, issue := range review.Issues {
    fmt.Printf("  [%s] %s:%d &#8212; %s\n", issue.Severity, issue.File, issue.Line, issue.Message)
}
if len(review.Issues) == 0 {
    fmt.Println("No issues found!")
}

if review.Status == "fail" {
    os.Exit(1)
}
</code></pre></div><p>If the status is <code>"fail"</code>, we exit with code 1, which tells Git to abort the commit. Simple and effective.</p><div><hr></div><h2>Installing the Hook</h2><p>There are fancier ways to manage pre-commit hooks &#8212; frameworks like <a href="https://pre-commit.com/">pre-commit</a> or tools like <a href="https://typicode.github.io/husky/">Husky</a>. But a pre-commit hook is really just an executable file, so manual installation is trivial.</p><p>Drop the following into <code>.git/hooks/pre-commit</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">#!/usr/bin/env bash
exec opencode-pre-commit</code></pre></div><p>Make sure the Go binary (<code>opencode-pre-commit</code>) is somewhere on your <code>$PATH</code>, and you&#8217;re set.</p><div><hr></div><h2>Testing It Out</h2><p>To see everything in action:</p><p><strong>1. Start the Opencode server:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">opencode serve --port 4096</code></pre></div><p><strong>2. Install the hook</strong> in any repo you want to test:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">vim .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
</code></pre></div><p><strong>3. Make a deliberately bad change</strong> (introduce a typo, leave a hardcoded secret, add a debug statement) and try to commit:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">git add .
git commit -m "hello people"
</code></pre></div><p>If the AI reviewer catches something, you&#8217;ll see output like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Reviewing staged changes...
Review status: fail
  [error] oc/main.go:15 &#8212; Hardcoded secret/API key in source code &#8212; remove and rotate
  [warning] oc/go.mod:3 &#8212; Go version 1.25.5 may cause build failures for older toolchains
  [info] oc/main.go:16 &#8212; baseURL is hardcoded &#8212; consider an ENV override for flexibility
exit status 1
</code></pre></div><p>The commit is blocked. Fix the issues, re-stage, and try again.</p><div><hr></div><h2>Things to Keep in Mind</h2><p>I used the Opus 4.6 model for testing and it consistently respected the JSON format and produced useful reviews. Other coding-focused models should work well too.</p><p>That said, a few caveats worth noting. Like any LLM output, the results aren&#8217;t deterministic &#8212; you may get slightly different feedback each time. You might also hit API rate limits depending on your usage. And while the model is surprisingly good at catching real issues, it can occasionally flag things that don&#8217;t matter. Treat it as a helpful second pair of eyes, not an infallible gatekeeper.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>Links</h2><ul><li><p><strong>Opencode</strong>: <a href="https://opencode.ai">opencode.ai</a></p></li><li><p><strong>Opencode Go SDK</strong>: <a href="https://github.com/anomalyco/opencode-sdk-go">github.com/anomalyco/opencode-sdk-go</a></p></li><li><p><strong>Source Code</strong>: <a href="https://github.com/plutov/opencode-pre-commit">github.com/plutov/opencode-pre-commit</a></p></li><li><p><a href="https://youtu.be/3j_Oh525Xrs">Watch on YouTube</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Developing a 2FA Desktop Client in Go]]></title><description><![CDATA[Building a 2FA Authenticator desktop client in Go and Wails+Vue]]></description><link>https://packagemain.tech/p/2fa-desktop-client-golang-wails-vue</link><guid isPermaLink="false">https://packagemain.tech/p/2fa-desktop-client-golang-wails-vue</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Sun, 15 Mar 2026 07:41:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f4e64ffd-2e71-48f3-8dff-7adc21802417_2400x1350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome back! Long ago I made a <a href="https://youtu.be/Dg9rUXxNV-c">video about Wails</a>, which is a framework/library to develop desktop or cross-platform apps in Go. And recently I wanted to revisit this project, because I wanted to build a local desktop 2FA Authenticator App, similar to Google/Microsoft Authenticator and others.</p><p>I was hoping that I could try the latest version 3, but it&#8217;s not yet released. But that shouldn&#8217;t be a problem as v2 is more than enough to create a lightweight desktop app.</p><p>Short recap of how Wails works, I won&#8217;t Go into the details, you can check it yourself:</p><blockquote><p>A Wails application is a standard Go application, with a webkit frontend. It is possible to bind Go methods to the frontend, and these will appear as JavaScript methods that can be called, just as if they were local JavaScript methods.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CREJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CREJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 424w, https://substackcdn.com/image/fetch/$s_!CREJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 848w, https://substackcdn.com/image/fetch/$s_!CREJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 1272w, https://substackcdn.com/image/fetch/$s_!CREJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CREJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp" width="1024" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10966,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/190967482?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CREJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 424w, https://substackcdn.com/image/fetch/$s_!CREJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 848w, https://substackcdn.com/image/fetch/$s_!CREJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 1272w, https://substackcdn.com/image/fetch/$s_!CREJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02223e6c-6e65-4401-84ee-408e5681c56f_1024x768.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Quite handy right? You can write desktop applications in Go. In terms of the frontend the following options are available:</p><ul><li><p>Svelte</p></li><li><p>React</p></li><li><p>Vue</p></li><li><p>Preact</p></li><li><p>Lit</p></li><li><p>Vanilla</p></li></ul><p>Also, it&#8217;s not Electron, it&#8217;s more lightweight and fast!</p><p>Could we vibe-code our 2FA App in under an hour with AI assistance? - Probably.</p><p>But would we actually learn how 2FA works? - No.</p><p>So let&#8217;s build this properly, understanding every piece, that&#8217;s the way of this newsletter.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h3>How 2FA actually works</h3><p>We all used 2FA apps.</p><p>Before we write a single line of code, we need to understand what we&#8217;re actually building. Most tutorials skip this part and just import a library. But that&#8217;s not how we learn.</p><p>When you enable 2FA, the server generates a random secret - just a long random string. This secret is shared ONCE between the server and your authenticator app, usually via QR code, in base32 format.</p><p>Example Secret JBSWY3DPEHPK3PXP (without padding)</p><p>Both the server and your app now have this secret. The server stores it in their database, your app stores it locally. This is the ONLY time they communicate about the secret.</p><p>The first &#8220;T&#8221; in TOTP stands for &#8220;Time-based&#8221;. TOTP usually refreshes every 30s.</p><blockquote><p>Current Time: 1707912345 (Unix timestamp in seconds)<br>Divide by 30: 56930411 (This is our counter)</p><p>Every 30 seconds, this counter increments by 1.</p></blockquote><p>Why 30 seconds? It&#8217;s a balance to get a counter.</p><p>Now we need to combine the secret and the counter in a way that:</p><ul><li><p>Can&#8217;t be reversed (you can&#8217;t figure out the secret from the code)</p></li><li><p>Is deterministic (same inputs = same output)</p></li><li><p>Is practically impossible to predict future codes</p></li></ul><p>This is where HMAC-SHA1 comes in. It&#8217;s a cryptographic hash function.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Secret Key + Counter &#8594; HMAC-SHA1 &#8594; 20-byte hash
Example:
Secret: JBSWY3DPEHPK3PXP
Counter: 56930411
Hash: 1f8698690e02ca16618550ef7f19da8e945b555a</code></pre></div><p>We have a 20-byte hash, but we need a 6-digit number. Can we just take the first 6 digits? No! That would be predictable.</p><p>Instead, we use the &#8220;dynamic truncation&#8221; - a fancy way of saying we pick a spot in the hash based on the hash itself:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Hash: 1f 86 98 69 0e 02 ca 16 61 85 50 ef 7f 19 da 8e 94 5b 55 5a

Last byte: 5a &#8594; Last 4 bits: 0xa (10 in decimal)
So we start at position 10 (0-indexed)

Extract 4 bytes starting at position 10:
Bytes: 50 ef 7f 19
Binary: 01010000 11101111 01111111 00011001
Integer: 1358806809

Take last 6 digits: 806809</code></pre></div><h3>The complete flow</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;                    USER'S APP                       &#9474;
&#9474;  Secret: JBSWY3DPEHPK3PXP (stored locally)          &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                        &#8595;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474;  Current Time: 1707912345     &#9474;
        &#9474;  Counter: 1707912345 / 30     &#9474;
        &#9474;         = 56930411            &#9474;
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                        &#8595;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474;  HMAC-SHA1(secret, counter)   &#9474;
        &#9474;  = 20-byte hash               &#9474;
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                        &#8595;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474;  Dynamic Truncation           &#9474;
        &#9474;  &#8594; 6-digit code: 806809       &#9474;
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                        &#8595;
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;                   SERVER                            &#9474;
&#9474;  (Does EXACT same calculation)                      &#9474;
&#9474;  Secret: JBSWY3DPEHPK3PXP (in database)             &#9474;
&#9474;  Code: 806809 &#9989; Match!                             &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</code></pre></div><h3>What about the QR code?</h3><p>So how does the secret get from the server to your app? QR codes!</p><p>The decode to the URL of the following format:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">otpauth://totp/GitHub:john@example.com?secret=JBSWY3DPEHPK3PXP&amp;issuer=GitHub
</code></pre></div><p>Let&#8217;s break this down:</p><ul><li><p><code>otpauth://</code> - URI scheme (tells apps this is for authentication)</p></li><li><p><code>totp</code> - Type (TOTP vs HOTP which is counter-based)</p></li><li><p><code>GitHub:john@example.com</code> - Label (shown in your app)</p></li><li><p><code>secret=JBSWY3DPEHPK3PXP</code> - The shared secret</p></li><li><p><code>issuer=GitHub</code> - Who&#8217;s providing the service</p></li></ul><p>Optional parameters you might see:</p><ul><li><p><code>algorithm=SHA1</code> (or SHA256, SHA512)</p></li><li><p><code>digits=6</code> (or 8)</p></li><li><p><code>period=30</code> (time window in seconds)</p></li></ul><p>Some applications let you set up the 2FA manually and show you the secret instead.</p><p>Now knowing the theory, let&#8217;s start building our app. it could be a CLI app, but why not to have a nice looking desktop app using Wails?</p><h3>Project setup</h3><p>We are only implementing the client side in this video, and will use external service as a server, for example <a href="https://totp.app">totp.app</a></p><p>I chose Vue.js and Typescript.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">go install github.com/wailsapp/wails/v2/cmd/wails@latest

wails init -n mfaclient -t vue-ts
cd mfaclient
wails dev</code></pre></div><p>Wails will generate some code for us, not much luckily, it&#8217;s still easy to navigate:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">mfaclient/
&#9500;&#9472;&#9472; app.go              # Main application logic
&#9500;&#9472;&#9472; main.go             # Entry point
&#9500;&#9472;&#9472; totp.go             # TOTP generation logic
&#9500;&#9472;&#9472; storage.go          # Encrypted storage for accounts
&#9500;&#9472;&#9472; frontend/App.vue.   # Our Vue frontend
&#9492;&#9472;&#9472; wails.json</code></pre></div><h3>Go implementation of TOTP</h3><p>The implementation will follow the algorithm described above and is quite straightforward. As you can see we only use the Go&#8217;s standard library.</p><p>totp.go</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">package main

import (
&#9;"crypto/hmac"
&#9;"crypto/sha1"
&#9;"encoding/base32"
&#9;"encoding/binary"
&#9;"fmt"
&#9;"strings"
&#9;"time"
)

func GenerateTotp(secret string) (string, int, error) {
&#9;now := time.Now()

&#9;counter := getCounter(now)

&#9;hash, err := calculateHash(secret, counter)
&#9;if err != nil {
&#9;&#9;return "", 0, err
&#9;}

&#9;code := truncate(hash)

&#9;remaining := 30 - (int(now.Unix()) % 30)

&#9;return fmt.Sprintf("%d", code), remaining, nil
}

func getCounter(t time.Time) uint64 {
&#9;return uint64(t.Unix()) / 30
}

func calculateHash(secret string, counter uint64) ([]byte, error) {
&#9;secret = strings.ReplaceAll(secret, " ", "")
&#9;secret = strings.TrimRight(secret, "=")
&#9;secret = strings.ToUpper(secret)

&#9;key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(secret)
&#9;if err != nil {
&#9;&#9;return nil, err
&#9;}

&#9;buf := make([]byte, 8)
&#9;binary.BigEndian.PutUint64(buf, counter)

&#9;h := hmac.New(sha1.New, key)
&#9;h.Write(buf)

&#9;return h.Sum(nil), nil
}

func truncate(hash []byte) int {
&#9;offset := hash[len(hash)-1] &amp; 0x0f

&#9;code := int(hash[offset]&amp;0x7f)&lt;&lt;24 |
&#9;&#9;int(hash[offset+1])&lt;&lt;16 |
&#9;&#9;int(hash[offset+2])&lt;&lt;8 |
&#9;&#9;int(hash[offset+3])

&#9;return code % 1000000
}</code></pre></div><p>The only hacky part that is there is the truncate function, which does some bit manipulation to get an integer.</p><p>Having this part coded, we can start thinking about how to store the 2FA accounts locally. And there are endless options here, we could store them in the database, could encrypt them and so forth. To make it simple for now, let&#8217;s store them into a plain JSON file locally.</p><p>storage.go</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">package main

import (
&#9;"crypto/rand"
&#9;"encoding/hex"
&#9;"encoding/json"
&#9;"os"
)

type Account struct {
&#9;Id     string `json:"id"`
&#9;Issuer string `json:"issuer"`
&#9;Secret string `json:"secret"`
}

type Storage struct {
&#9;Filepath string
}

func generateID() string {
&#9;bytes := make([]byte, 8)
&#9;rand.Read(bytes)
&#9;return hex.EncodeToString(bytes)
}

func (s *Storage) save(accounts []Account) error {
&#9;data, err := json.Marshal(accounts)
&#9;if err != nil {
&#9;&#9;return err
&#9;}

&#9;return os.WriteFile(s.Filepath, data, 0o600)
}

func (s *Storage) LoadAccounts() ([]Account, error) {
&#9;data, err := os.ReadFile(s.Filepath)
&#9;if err != nil {
&#9;&#9;if os.IsNotExist(err) {
&#9;&#9;&#9;return []Account{}, nil
&#9;&#9;}

&#9;&#9;return nil, err
&#9;}

&#9;var accounts []Account
&#9;json.Unmarshal(data, &amp;accounts)
&#9;return accounts, nil
}

func (s *Storage) AddAccount(a Account) error {
&#9;accounts, err := s.LoadAccounts()
&#9;if err != nil {
&#9;&#9;return err
&#9;}

&#9;a.Id = generateID()
&#9;accounts = append(accounts, a)
&#9;return s.save(accounts)
}

func (s *Storage) DeleteAccount(id string) error {
&#9;accounts, err := s.LoadAccounts()
&#9;if err != nil {
&#9;&#9;return err
&#9;}

&#9;filtered := []Account{}
&#9;for _, acc := range accounts {
&#9;&#9;if acc.Id != id {
&#9;&#9;&#9;filtered = append(filtered, acc)
&#9;&#9;}
&#9;}

&#9;return s.save(filtered)
}</code></pre></div><h3>Hooking up with frontend</h3><p>Once backend Go part is ready we can start exposing these methods as bindings from app.go and use them in frontend/App.vue</p><p>I will not share the whole code, you can see the link at the end of the article.</p><p>But developing the Wails frontend feels very similar to developing in Vue on the web, here is the snippet from our App.vue:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">&lt;script lang="ts" setup&gt;
import { ref, onMounted, onUnmounted } from "vue";
import { GetAccounts, AddAccount, DeleteAccount } from "../wailsjs/go/main/App";
import { main } from "../wailsjs/go/models";

const accounts = ref&lt;main.AccountWithCode[]&gt;([]);
const error = ref("");
const showForm = ref(false);
const issuer = ref("");
const secret = ref("");
const addError = ref("");
const copiedId = ref("");

// ...</code></pre></div><h3>Testing</h3><p>Secrets are always distributed in base32 format, so let&#8217;s generate one random first:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">openssl rand -hex 10 | base32</code></pre></div><p>Wails has a nice dev mode where it compiles all the parts of the app and has a hot reload for all components, be it backend or frontend.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">wails dev</code></pre></div><p>Now we can see our shiny app is working and generating the valid TOTP codes. We confirm it by using the same secrets on <a href="https://totp.app">totp.app</a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qtOF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qtOF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 424w, https://substackcdn.com/image/fetch/$s_!qtOF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 848w, https://substackcdn.com/image/fetch/$s_!qtOF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 1272w, https://substackcdn.com/image/fetch/$s_!qtOF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qtOF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png" width="1030" height="1490" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1490,&quot;width&quot;:1030,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:165461,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/190967482?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qtOF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 424w, https://substackcdn.com/image/fetch/$s_!qtOF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 848w, https://substackcdn.com/image/fetch/$s_!qtOF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 1272w, https://substackcdn.com/image/fetch/$s_!qtOF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b02dc06-88c6-491d-8fa8-61398e004d95_1030x1490.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We could also test it on a real service, but I suggest not trying it yet with your real accounts. Instead, you can run something like <a href="https://www.keycloak.org/">Keycloak</a> locally.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev</code></pre></div><h3>Conclusion</h3><p>Obviously this can be compiled so we have everything in a single binary.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">wails build</code></pre></div><p>This app needs many improvements such as: better UI, more secure account storage, support for different algorithms.</p><p>Otherwise Wails has proven once again to be a simple library to build lightweight desktop apps.</p><p>Links:</p><ul><li><p><a href="https://github.com/wailsapp/awesome-wails">Apps built with Wails</a></p></li><li><p><a href="https://github.com/plutov/packagemain/tree/main/2fa/mfaclient">Source code</a></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Golang optimizations for high‑volume services]]></title><description><![CDATA[Lessons from a Postgres &#8594; Elasticsearch pipeline]]></description><link>https://packagemain.tech/p/golang-optimizations-for-highvolume</link><guid isPermaLink="false">https://packagemain.tech/p/golang-optimizations-for-highvolume</guid><dc:creator><![CDATA[Julien Singler]]></dc:creator><pubDate>Mon, 08 Dec 2025 11:48:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HiPm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HiPm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HiPm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!HiPm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!HiPm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!HiPm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HiPm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1583190,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/180879519?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HiPm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!HiPm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!HiPm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!HiPm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb56a59d5-f31c-424d-a3fa-0eff50005175_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Intro</h2><p>Building services that sit on top of a Postgres replication slot and continuously stream data into Elasticsearch is a great way to get low&#8209;latency search without hammering your primary database with ad&#8209;hoc queries. But as soon as traffic ramps up, these services become a stress test for Go&#8217;s memory allocator, garbage collector, and JSON stack.</p><p>This post walks through optimizations applied to a real-world service that:</p><ul><li><p>Connects to a Postgres replication slot</p></li><li><p>Transforms and enriches the change events</p></li><li><p>Uses Elasticsearch&#8217;s bulk indexer to index and delete documents</p></li></ul><p>The constraints: the service cannot stop reading from the replication slot for long (or Postgres disk will grow), and it cannot buffer unbounded data in memory (or Go&#8217;s heap will). The goal is to keep latency and memory stable under sustained high volume.</p><h2>The core problem: unbounded streams under tight constraints</h2><p>Replication slots are relentless: as long as your primary receives writes, the slot will keep producing changes. If your consumer slows down, Postgres has to retain more WAL segments, increasing disk usage on the database server. If your consumer tries to &#8220;just buffer more&#8221; in memory, the heap will balloon and garbage collection will kick in more frequently, stealing CPU from useful work.</p><p>In this setup, you typically have three competing forces:</p><ul><li><p>Backpressure from Elasticsearch bulk indexing</p></li><li><p>The continuous stream of changes from the replication slot</p></li><li><p>The Go runtime&#8217;s allocator and garbage collector trying to keep up with allocations in your hot path</p></li></ul><p>The design work is about turning this into a stable flow: limit in-flight work, keep memory usage predictable, and reduce per-message overhead.</p><h2>JSON performance: why switch from encoding/json to jsoniter</h2><p>One of the earliest hot spots in services like this is JSON encoding/decoding for documents going into Elasticsearch. The standard library&#8217;s <code>encoding/json</code> is correct and convenient, but it trades some performance for safety and reflection-based flexibility.</p><p>High-volume services often switch to <code>jsoniter</code> (github.com/json-iterator/go) for a few reasons:</p><ul><li><p>Faster encoding/decoding for common patterns</p></li><li><p>Less reflection overhead when configured with codegen or field caching</p></li><li><p>Better throughput when serializing large batches of similar structs</p></li></ul><p>The wins are most visible when:</p><ul><li><p>You are serializing many small documents at high frequency (typical for bulk indexing)</p></li><li><p>You can keep your types stable and avoid excessive use of <code>interface{}</code> and maps</p></li><li><p>You care about reducing allocations in the JSON path and shaving microseconds off each object</p></li></ul><p>However, replacing <code>encoding/json</code> is never just a drop-in optimization; it changes behavior in subtle ways, especially around nulls and omitted fields.</p><h3>Jsoniter compatibility</h3><p>Jsoniter provides different configurations, one of them is the <code>ConfigCompatibleWithStandardLibrary which attempt to be generating a really similar payload than the standard library. </code></p><p>There is, however, some edge cases. </p><p>Here one I went through:</p><ul><li><p>Jsoniter doesn&#8217;t seems to work well with the json tag &#8220;omitzero&#8221; and libraries like guregu/null.v4. Prefer using &#8220;omitempty&#8221; to have the same behavior because jsoniter will check the .Valid() method. </p></li></ul><p>All in all, this is a drop in replacement, but adding some tests to make sure you don&#8217;t have side effects in complex systems will make a big difference. </p><h2>Controlling allocations with sync.Pool</h2><p>Once the JSON serialization hot path is reasonably efficient, memory allocations often become the next bottleneck. Every replication event that flows through your service may involve:</p><ul><li><p>Allocating a struct to represent the change</p></li><li><p>Allocating buffers for JSON encoding</p></li><li><p>Allocating intermediate slices and maps during transformations</p></li></ul><p>Under sustained load, this can produce a flood of short-lived objects. The garbage collector has to scan and reclaim them, and that work shows up as CPU usage and latency spikes.</p><p><code>sync.Pool</code> is a practical tool for these patterns:</p><ul><li><p>It lets you reuse objects (structs, buffers, small slices) across requests without manual &#8220;object lifecycle&#8221; tracking</p></li><li><p>Objects in the pool are eligible for garbage collection if they are not in use, so the pool does not create a permanent memory leak</p></li><li><p>For hot types (e.g., your &#8220;replication event&#8221; struct or reusable <code>[]byte</code> buffers for JSON encoding), pooling can significantly reduce the number of allocations per event</p></li></ul><p>Good use cases in this pipeline include:</p><ul><li><p>Reusing buffers for building bulk requests (e.g., <code>bytes.Buffer</code> or <code>[]byte</code>)</p></li><li><p>Reusing small structs that hold metadata for each change event</p></li><li><p>Reusing temporary scratch space used during transformations</p></li></ul><p>Some practical guidelines:</p><ul><li><p>Only pool objects that are frequently allocated and easy to reset to a zero state</p></li><li><p>Add helper methods like <code>Reset()</code> so the code path that returns an object to the pool always leaves it in a clean state</p></li><li><p>Avoid pooling objects that embed context, locks, or anything with complex lifecycle or ownership semantics</p></li></ul><p>Used carefully, <code>sync.Pool</code> can cut heap allocations dramatically in a high-throughput Go service, which brings GC frequency and pause times down.</p><h2>Garbage collection tuning and experimental GCs</h2><p>Even after optimizing allocations, GC behavior remains critical in long-lived services under high load. </p><p>Starting with go1.25, you can enable an experimental GC at build time that promise better performances. https://go.dev/blog/greenteagc</p><ul><li><p>Reduce GC-induced latency spikes in services that care more about throughput and tail latency than about absolute minimal memory usage</p></li><li><p>Provide more even performance under bursts by scheduling GC work more smoothly over time</p></li></ul><p>In a pipeline that must keep up with a replication slot and a bulk indexer, that trade-off is often desirable:</p><ul><li><p>Slightly higher steady-state memory usage is acceptable if it avoids GC pauses that temporarily slow down ingestion</p></li><li><p>Less erratic latency helps keep Elasticsearch batches flowing and prevents backpressure from building up</p></li></ul><p>However, tuning or switching GC behavior should be the last step, not the first. It works best when:</p><ul><li><p>Allocations in hot paths have already been reduced with pooling, pre-allocation, and careful data structures</p></li><li><p>JSON and other serialization work has been profiled and streamlined</p></li><li><p>The service has clear SLOs around memory and latency, and you are comfortable trading one for the other within defined bounds</p></li></ul><p>GC tweaks can then be used to shift the balance slightly rather than to compensate for fundamentally inefficient code.</p><div><hr></div><h2>Putting it together: a stable, high&#8209;volume change pipeline</h2><p>When all of these optimizations come together, the architecture looks like this:</p><ul><li><p>A controlled number of goroutines read from the replication slot and push events through a bounded internal queue</p></li><li><p>Each event passes through transformation and enrichment code that avoids unnecessary allocations and uses <code>sync.Pool</code> for reusable buffers and structs</p></li><li><p>JSON serialization uses a faster encoder like <code>jsoniter</code></p></li><li><p>A bulk indexer sends batched operations to Elasticsearch with size and concurrency tuned to avoid large in-memory batches but still keep the cluster saturated</p></li><li><p>GC behavior is tuned only after profiling, to smooth out remaining latency spikes without blowing out memory usage</p></li></ul><p>The result is a Go service that can keep up with a constant stream of database changes, avoid unbounded buffering, and make efficient use of CPU and memory&#8212;all while staying within the operational constraints of Postgres and Elasticsearch.</p>]]></content:encoded></item><item><title><![CDATA[ULID: Universally Unique Lexicographically Sortable Identifier]]></title><description><![CDATA[Using ULID identifiers in Go programs running on Postgres database.]]></description><link>https://packagemain.tech/p/ulid-identifier-golang-postgres</link><guid isPermaLink="false">https://packagemain.tech/p/ulid-identifier-golang-postgres</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Mon, 01 Dec 2025 20:30:05 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1633f002-8754-437f-b977-f8637217ddaf_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <strong>UUID</strong> format is a highly popular and amazing standard for unique identifiers. However, despite its ubiquity, it can be suboptimal for many common use-cases because of several inherent limitations:</p><ul><li><p>It isn&#8217;t the most character efficient or human-readable.</p></li><li><p><strong>UUID v1/v2</strong> is impractical in many environments, as it requires access to a unique, stable MAC address.</p></li><li><p><strong>UUID v3/v5</strong> requires a unique seed.</p></li><li><p><strong>UUID v4</strong> provides no other information than true randomness, which can lead to database fragmentation in data structures like B-trees, ultimately hurting write performance.</p></li></ul><h2>Introducing the ULID Identifier</h2><p>Few projects I worked on used the <strong>ULID (Universally Unique Lexicographically Sortable Identifier)</strong>, and I really enjoyed working with it, and would love to share this experience with you. Specifically for Go programs using Postgres database. But the same applies to other languages or databases too.</p><p>You can find the full spec here - <a href="https://github.com/ulid/spec">github.com/ulid/spec</a></p><pre><code>ulid() // 01ARZ3NDEKTSV4RRFFQ69G5FAV</code></pre><h2>What makes ULID great?</h2><p>ULID addresses the drawbacks of traditional UUID versions by focusing on four key characteristics:</p><ul><li><p>Lexicographically sortable. Yes, you can sort the IDs. This is the single biggest advantage for database indexing.</p></li><li><p>Case insensitive.</p></li><li><p>No special characters (URL safe).</p></li><li><p>It&#8217;s compatible with UUID, so you can still use native UUID columns in your database, for example.</p></li></ul><p>ULID&#8217;s structure is key to its sortability. It is composed of 128 bits, just like a UUID, but those bits are structured for function: 48 bits of timestamp followed by 80 bits of cryptographically secure randomness.</p><pre><code> 01AN4Z07BY      79KA1307SR9X4MV3

|----------|    |----------------|
 Timestamp          Randomness
   48bits             80bits</code></pre><h2>Go+Postgres example</h2><p>The power of ULID is its seamless integration into existing systems, even those relying on the UUID data type. Here is a demonstration using Go with the popular <code>pgx</code><a href="https://github.com/jackc/pgx"> driver</a> for PostgreSQL and the <a href="https://github.com/oklog/ulid">oklog/ulid</a> package.</p><p>The code below first connects to a running PostgreSQL instance and creates a table where the primary key is of type <code>UUID</code>. We then insert records using both standard UUID v4 and ULID.</p><pre><code>package main

import (
  "context"
  "fmt"
  "os"

  "github.com/google/uuid"
  "github.com/jackc/pgx/v5"
  "github.com/oklog/ulid/v2"
)

func main() {
  ctx := context.Background()

  conn, err := pgx.Connect(ctx, "postgres://...")
  if err != nil {
    panic(err)
  }
  defer conn.Close(ctx)

  _, err = conn.Exec(ctx, `
CREATE TABLE IF NOT EXISTS ulid_test (
  id UUID PRIMARY KEY,
  kind TEXT NOT NULL,
  value TEXT NOT NULL
);`)
  if err != nil {
    panic(err)
  }

  insertUUID(ctx, conn, &#8220;1&#8221;)
  insertUUID(ctx, conn, &#8220;2&#8221;)
  insertUUID(ctx, conn, &#8220;3&#8221;)
  insertUUID(ctx, conn, &#8220;4&#8221;)
  insertUUID(ctx, conn, &#8220;5&#8221;)

  insertULID(ctx, conn, &#8220;1&#8221;)
  insertULID(ctx, conn, &#8220;2&#8221;)
  insertULID(ctx, conn, &#8220;3&#8221;)
  insertULID(ctx, conn, &#8220;4&#8221;)
  insertULID(ctx, conn, &#8220;5&#8221;)
}

func insertUUID(ctx context.Context, conn *pgx.Conn, value string) {
  id := uuid.New()
  conn.Exec(ctx, "INSERT INTO ulid_test (id, value, kind) VALUES ($1, $2, 'uuid')", id, value)

  fmt.Printf("Inserted UUID: %s\n", id.String())
}

func insertULID(ctx context.Context, conn *pgx.Conn, value string) {
  id := ulid.Make()

  // as you can see, we don&#8217;t need to format the ULID as a string, it can be used directly
  conn.Exec(ctx, "INSERT INTO ulid_test (id, value, kind) VALUES ($1, $2, 'ulid')", id, value)

  fmt.Printf("Inserted ULID: %s\n", id.String())
}</code></pre><p>The oklog/ulid package implements the necessary interfaces (specifically, database/sql/driver.Valuer and encoding.TextMarshaler) that allow it to be automatically converted into a compatible format (like a string or []byte representation of the UUID) that the pgx driver can successfully map to the PostgreSQL UUID column type. This allows developers to leverage the sortable advantages of ULID without having to change the underlying database schema type in many popular environments.</p><p>This allows developers to leverage the sortable advantages of ULID without having to change the underlying database schema type in many popular environments.</p><h2>Sortability and Performance</h2><p>The time-based prefix means that new ULIDs will always be greater than older ULIDs, ensuring that records inserted later will be physically placed at the end of the index. This contrasts sharply with UUID v4, where the sheer randomness means records are scattered throughout the index structure.</p><p>With traditional UUID v4, sorting records by their insert time is not possible without an extra column. When using ULID, the sort order is inherent in the ID itself, as demonstrated by the following database query output:</p><pre><code>select * from ulid_test where kind = 'ulid' order by id;

019aaae4-be9c-d307-238f-be1692b3e8d7 | ulid | 1
019aaae4-be9d-011f-b82e-b870ca2abe9d | ulid | 2
019aaae4-be9f-e9d7-6efc-5b298ecc572b | ulid | 3
019aaae4-bea0-deae-6408-d89e7e3ce030 | ulid | 4
019aaae4-bea1-8ed2-c2f5-144bb1ffedde | ulid | 5</code></pre><p>As we can see, the records are returned in the same order they were inserted. Furthermore, ULID is much shorter and cleaner when used in contexts like a URL:</p><pre><code><code>/users/01KANDQMV608PBSMF7TM9T1WR4</code></code></pre><p>ULID can generate 1.21e+24 unique ULIDs per millisecond, which should be more than enough for most applications.</p><h2>Limitations and the future</h2><p>There&#8217;s really no major drawback to using ULID, but you should understand its limitations. For very (and I mean <strong>very</strong>) high-volume write systems, ULIDs can become problematic. Since all writes are clustered around the current timestamp, you will have <strong>hot spots</strong> around the current index key, which can potentially lead to slower writes and increased latency on that specific index block.</p><p>While other alternative identifiers exist, such as <strong>CUID</strong> or <strong>NanoID</strong>, the benefits of ULID have become a major factor in the evolution of unique identifier standards.</p><p>It is worth noting that the newest proposed standard for unique identifiers, <strong>UUID v7</strong>, aims to address the sortability and database performance issues of older UUID versions by adopting a similar time-ordered structure to ULID.</p><h2>Resources</h2><ul><li><p><a href="https://github.com/ulid/spec">https://github.com/ulid/spec</a></p></li><li><p><a href="https://github.com/oklog/ulid">https://github.com/oklog/ulid</a></p></li><li><p><a href="https://github.com/jackc/pgx">https://github.com/jackc/pgx</a></p></li><li><p><a href="https://www.youtube.com/watch?v=otW7nLd8P04">Watch on our Youtube channel</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Building Conway’s Game of Life in Go with raylib-go]]></title><description><![CDATA[A step-by-step tutorial on building Conway&#8217;s Game of Life in Go, using raylib-go for graphics. Learn how to draw grids, apply the rules of life, and simulate evolving patterns.]]></description><link>https://packagemain.tech/p/building-conways-game-of-life-in</link><guid isPermaLink="false">https://packagemain.tech/p/building-conways-game-of-life-in</guid><dc:creator><![CDATA[Tim Little]]></dc:creator><pubDate>Mon, 22 Sep 2025 09:11:55 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5c10c9d4-788c-4f0d-a7c8-aeb94c864962_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently started to play around with graphics programming and game engine creation. For that, I was using SDL2, OpenGL and C.</p><p>However, I wanted to do something in Go, so I started with <a href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life">Conway&#8217;s Game of Life</a>. There are various options from C bindings to <a href="https://github.com/go-gl/gl">OpenGL</a> and <a href="https://github.com/veandco/go-sdl2">SDL2</a> to full game engines such as <a href="https://ebitengine.org/">Ebitengine</a>. I went with <a href="https://github.com/gen2brain/raylib-go">raylib-go</a> as it was a good mixture of low-level and abstracted programming.</p><p>In this tutorial, we&#8217;ll use <strong>Go</strong> and <strong>raylib-go</strong> to create Conway&#8217;s Game of Life, a simulation where simple rules create endless patterns. By the end, you&#8217;ll have gliders drifting, pulsars pulsing, and even a glider gun firing across your screen.</p><h3><strong>The Game of Life</strong></h3><p>Conway&#8217;s Game of Life is a cellular automaton created by John Conway. It is a zero-player game which evolves. Each generation is built on the previous generation using a set of simple rules.</p><p>The game requires no players as you start it off, and it starts to evolve without intervention. It is <a href="https://en.wikipedia.org/wiki/Turing_completeness">Turing Complete</a> and can simulate a wide variety of different patterns and Turing Machines.</p><p>It starts with an initial game start, then it uses the following rules to determine which cell lives or dies:</p><ol><li><p>Any live cell with <strong>fewer than 2</strong> live neighbours dies, as if by underpopulation.</p></li><li><p>Any <strong>live cell with 2 or 3</strong> live neighbours lives on to the next generation.</p></li><li><p>Any live cell with <strong>more than 3</strong> live neighbours dies, as if by overpopulation.</p></li><li><p>Any dead cell with <strong>exactly 3 live</strong> neighbours becomes a live cell, as if by reproduction.</p></li></ol><p>Each generation or frame in the context of our game will use these rules to determine the next state.</p><h3><strong>Demo</strong></h3><p>In this tutorial, we will be creating a simple cellular automation in Go using raylib-go. It will start with a state we define, and allow us to modify the state to see different evolutions.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zHEr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zHEr!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zHEr!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif" width="800" height="430" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:430,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!zHEr!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Game of life</figcaption></figure></div><h3><strong>Setting up the project</strong></h3><p>First thing we need to do is create a go package for our project and pull down raylib-go so we can start to build with it.</p><pre><code>mkdir go-gameoflife  
go mod init gameoflife  
go get -v -u github.com/gen2brain/raylib-go/raylib</code></pre><p>When learning anything new in programming, we always start with the classic &#8220;Hello World&#8221;. To do that, we need to import raylib-go, create a window and display some text.</p><pre><code>package main  
  
import rl "github.com/gen2brain/raylib-go/raylib"  
  
type Game struct {  
 Height int  
 Width  int  
}  
  
func main() {  
 // Create the game metadata and state holding object  
 game := Game{Width: 800, Height: 400}  
  
 // Create the Raylib window using the state  
 rl.InitWindow(int32(game.Width), int32(game.Height), "Game of life")  
  
 // Close the window at the end of the program  
 defer rl.CloseWindow()  
  
 // We dont need a high FPS for the game, so 10 should be enough  
 rl.SetTargetFPS(10)  
  
 // Loop until the window needs to close  
 for !rl.WindowShouldClose() {  
  // Starting drawing to the canvas  
  rl.BeginDrawing()  
  
  // Create a black background  
  rl.ClearBackground(rl.Black)  
  
  // Draw Hello world  
  rl.DrawText("Hello world!", 350, 200, 20, rl.RayWhite)  
  
  // End the drawing  
  rl.EndDrawing()  
 }  
}</code></pre><p>We have also created a struct to store the game metadata, and we can later use it to store the state of the game. We use that to initialise the window.</p><p>To save a little bit of time, we can create a Makefile, which will allow us to simplify the building and running of our code. In this case, we only need one target <strong>run,</strong> however, this can be expanded to run tests, build more files, clean up build states and more.</p><pre><code>.DEFAULT_GOAL := run  
  
run:  
 go run .  
  
help:  
 @echo "run - run the game"  
  
.PHONY: run</code></pre><p>Now that we have the make file, we can run make to start the program. The same could be done by running. <code>go run .</code></p><pre><code><code>make</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HAQv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HAQv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 424w, https://substackcdn.com/image/fetch/$s_!HAQv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 848w, https://substackcdn.com/image/fetch/$s_!HAQv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 1272w, https://substackcdn.com/image/fetch/$s_!HAQv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HAQv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp" width="1400" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!HAQv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 424w, https://substackcdn.com/image/fetch/$s_!HAQv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 848w, https://substackcdn.com/image/fetch/$s_!HAQv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 1272w, https://substackcdn.com/image/fetch/$s_!HAQv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc1d37dd-5180-4de3-82fc-55d7010df312_1400x829.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Hello world running in raylib-go</figcaption></figure></div><p>Win! We now have a window with our Hello World.</p><p>In the background, raylib is doing all the hard work for us, working with OpenGL to create the window in the system-specific libraries for your operating system. However, we do not need to worry about that.</p><p>For each step of this tutorial, I will provide the code so you can compare. The full code for this step is in the <a href="https://github.com/timlittle/blog-code/blob/87e511b14d464d023c870ad3dd77f83b6083350c/go-gameoflife/main.go">GitHub Repo</a>.</p><h4><strong>Creating a 2D map</strong></h4><p>The next thing we need to do is to create an initial state for our game, which will consist of a 2D matrix which we use to draw the cells to the screen.</p><p>We will need to update our Game struct to store our state and to specify how big we want the cells.</p><pre><code> type Game struct {  
-       Height int  
-       Width  int  
+       Height   int  
+       Width    int  
+       tileSize int  
+       State    [][]int  
+}</code></pre><p>We can then create a function to create the new start for us</p><pre><code>func NewGame(width, height, tileSize int) *Game {  
 g := &amp;Game{Width: width, Height: height, tileSize: tileSize}  
 g.State = [][]int{  
  {0, 1, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 1, 0, 0, 0, 0, 1, 0, 0},  
  {1, 1, 1, 0, 0, 0, 0, 1, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 1, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
 }  
 return g  
}  </code></pre><p>Next, we need a way to display this on our window. We can simply loop through the array and check if the cell is alive. If it is, we can draw the cell to the screen. We do this with a <code>Draw()</code> method on the <strong>Game</strong> struct.</p><pre><code>func (g *Game) Draw() {  
 // Loop through all of the rows  
 for y := range g.State {  
  
  // Loop through all of the columns  
  for x := 0; x &lt; len(g.State[y]); x++ {  
  
   // If we have marked the column as a 1, draw it as white  
   if g.State[y][x] == 1 {  
  
    // We will need to scale our blocks to the size of the window  
    pixelX := x * g.tileSize  
    pixelY := y * g.tileSize  
  
    // Draw the block to the screen  
    rl.DrawRectangle(int32(pixelX), int32(pixelY), int32(g.tileSize), int32(g.tileSize), rl.RayWhite)  
   }  
  }  
 }  
}</code></pre><p>Now we need to update our <code>main()</code>function to use the new game state, then draw it to the window.</p><pre><code>-       game := Game{Width: 800, Height: 400}  
+       var game = NewGame(800, 400, 80)  
  
...  
  
-               // Draw Hello world  
-               rl.DrawText("Hello world!", 350, 200, 20, rl.RayWhite)  
+               // Draw the game state  
+               game.Draw()</code></pre><p>Now we can run the game again.</p><pre><code><code>make</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UETV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UETV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 424w, https://substackcdn.com/image/fetch/$s_!UETV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 848w, https://substackcdn.com/image/fetch/$s_!UETV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 1272w, https://substackcdn.com/image/fetch/$s_!UETV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UETV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp" width="1400" height="829" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:829,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!UETV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 424w, https://substackcdn.com/image/fetch/$s_!UETV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 848w, https://substackcdn.com/image/fetch/$s_!UETV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 1272w, https://substackcdn.com/image/fetch/$s_!UETV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F382d22d9-9bd9-4cfb-aef6-a7c72433ad22_1400x829.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Starting state</figcaption></figure></div><p>We now have the starting state for our game. I have used a cell size of 80x80px here to allow us to render it in an 800x400px window, so we can play with the logic and see the results.</p><p>Full code for this step is in the <a href="https://github.com/timlittle/blog-code/blob/379b93addb38c711377209fef1c7ee7a0569901e/go-gameoflife/main.go">GitHub Repo</a></p><h4><strong>Getting neighbours</strong></h4><p>Now we get to the part where we bring our game to life. We can start to create the rule to evolve the state.</p><p>To do this, we need a way of counting how many live neighbours a cell has. We can do this by looping through all of the neighbours and checking the surrounding cells.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!okdA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!okdA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 424w, https://substackcdn.com/image/fetch/$s_!okdA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 848w, https://substackcdn.com/image/fetch/$s_!okdA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 1272w, https://substackcdn.com/image/fetch/$s_!okdA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!okdA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg" width="360" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:360,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!okdA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 424w, https://substackcdn.com/image/fetch/$s_!okdA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 848w, https://substackcdn.com/image/fetch/$s_!okdA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 1272w, https://substackcdn.com/image/fetch/$s_!okdA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5783471a-4f28-4baa-9bbf-d6ed6f06c89d_360x280.svg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Logic for checking neighbours</figcaption></figure></div><p>We can start with the top left cell, the move to the next until we have covered all the cells. We will need to exclude the current cell and account for whether the cell we are calculating is at the edge of the window.</p><pre><code>func CountNeighbours(x, y int, gameState [][]int) int {  
 // Counter for the neighbours  
 count := 0  
  
 // Loop through all the rows  
 for cellX := x - 1; cellX &lt;= x+1; cellX++ {  
  
  // Loop through all the columns  
  for cellY := y - 1; cellY &lt;= y+1; cellY++ {  
  
   // We want to make sure we do not count past the boundary of the board  
   if cellY &lt; 0 || cellX &lt; 0 || cellY &gt;= len(gameState) || cellX &gt;= len(gameState[0]) {  
    continue  
   }  
   // If current cell, we can skip it  
   if cellY == y &amp;&amp; cellX == x {  
    continue  
   }  
   //  Check if cell is alive  
   if gameState[cellY][cellX] == 1 {  
    count++  
   }  
  
  }  
 }  
 return count  
}</code></pre><p>Next, we need to codify the rules of the game using the count of the neighbours. We can do that with a <code>switch</code> statement for each of the rules.</p><pre><code>func IsCellAlive(current, neighbours int) int {

 switch {

 // Any live cell with fewer than two live neighbours dies  
 // as if by underpopulation.  
 case neighbours &lt; 2:  
  return 0

 // Any live cell with two or three live neighbours lives  
 // on to the next generation.  
 // Any dead cell with two neighbours, remains dead  
 case neighbours == 2:  
  return current

 // Any dead cell with exactly three live neighbours becomes a  
 // live cell, as if by reproduction.  
 case neighbours == 3:  
  return 1

 // Any live cell with more than three live neighbours dies  
 // as if by overpopulation.  
 case neighbours &gt; 3:  
  return 0  
 }
 
 return 0  
}</code></pre><p>Now that we have the calculations for our next state implemented, we need to take our existing state and update it based on the rules. We can create an <code>Update()</code> method on our Game struct to do this.</p><pre><code>func (g *Game) Update() {  
  
 // We can stat with hardcoded state for now  
 // However, we would want to update thisbased on the current state  
 var newState = [][]int{  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
 }  
  
 // Loop through each row  
 for indexY, cellY := range g.State {  
  
  // Loop through each column  
  for indexX, cellX := range cellY {  
     
   // Count how many neighbours the current cell has  
   neighbours := CountNeighbours(indexX, indexY, g.State)  
  
   // Update the new state using the rule based on neighbours  
   newState[indexY][indexX] = IsCellAlive(cellX, neighbours)  
  }  
 }  
  
 // Set the new state to the updated state  
 g.State = newState  
}</code></pre><p>We have used another slicee to build the new state, then we replace the old one. This implementation increases memory usage, however, we could offset and update it in-place with a truth table and two passes of the slice. However, let's keep it simple.</p><pre><code>rl.ClearBackground(rl.Black)  
  
+               // Update the game state before drawing  
+               game.Update()  
+  
                // Draw the game state</code></pre><p>Now we can add the Update function to our main game loop and start the game again to see if it works.</p><pre><code><code>make</code></code></pre><p>Run it and you will see the cells start to evolve!</p><p>We can see the patterns start to evolve with each generation, and they will converge into a steady state eventually.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!C-oE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!C-oE!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!C-oE!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!C-oE!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!C-oE!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!C-oE!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif" width="800" height="430" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:430,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!C-oE!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!C-oE!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!C-oE!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!C-oE!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc78e26-74f9-468e-8eba-6eaffbc0d6b8_800x430.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Conway&#8217;s game of life with updates</figcaption></figure></div><p>Full code for this step is in the <a href="https://github.com/timlittle/blog-code/blob/1d8b8944f90046cc7e61d7c13068f493b25f2d21/go-gameoflife/main.go">GitHub Repo</a></p><h4><strong>Update the map</strong></h4><p>Now that the core functionality of our game is working, we can start to scale the map size to make more complex patterns.</p><p>Let's replace the hard-coded map with a function we can use to generate a game state as big as we want.</p><pre><code>func CreateGameState(newWidth, newHeight int) [][]int {  
 // Create a new game state with the right height  
 newState := make([][]int, newHeight)  
  
 // Create the rows with the right length  
 for i := range newHeight {  
  newState[i] = make([]int, newWidth)  
 }  
  
 // Return the new state map  
 return newState  
}</code></pre><p>We can use this blank game state in our code where we are manually defining the state. We are defining the state in both the <code>Update()</code> and <code>NewGame()</code> functions.</p><pre><code>        // However, we would want to update this based on the current state  
-       var newState = [][]int{  
-               {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
-               {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
-               {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
-               {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
-               {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
-       }  
+       newState := CreateGameState(len(g.State[0]), len(g.State))  
  
        // Loop through each row</code></pre><pre><code> func NewGame(width, height, tileSize int) *Game {  
        g := &amp;Game{Width: width, Height: height, tileSize: tileSize}  
-       g.State = [][]int{  
-               {0, 1, 0, 0, 0, 0, 0, 0, 0, 0},  
-               {0, 0, 1, 0, 0, 0, 0, 1, 0, 0},  
-               {1, 1, 1, 0, 0, 0, 0, 1, 0, 0},  
-               {0, 0, 0, 0, 0, 0, 0, 1, 0, 0},  
-               {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
-       }  
+       g.State = CreateGameState(g.Width/g.tileSize, g.Height/g.tileSize)  
        return g  
 }</code></pre><p>That looks much cleaner. However, we have now lost our initial state, which triggers the game. We can fix this by creating a function that will add a new pattern. We will start with the <a href="https://en.wikipedia.org/wiki/Glider_%28Conway%27s_Game_of_Life%29">Glider</a> pattern.</p><pre><code>func CreateGliders(x, y int, gameState *[][]int) {  
 // Draw the glider patter in the game state  
 (*gameState)[y][x+1] = 1  
 (*gameState)[y+1][x+2] = 1  
 (*gameState)[y+2][x] = 1  
 (*gameState)[y+2][x+1] = 1  
 (*gameState)[y+2][x+2] = 1  
}</code></pre><p>Now that we have the function to create a Glider, we can add them to our game state in the <code>main()</code> function.</p><pre><code>-       var game = NewGame(800, 400, 80)  
+       var game = NewGame(800, 400, 10)  
+       CreateGliders(0, 0, &amp;game.State)  
+       CreateGliders(10, 0, &amp;game.State)  
+       CreateGliders(20, 0, &amp;game.State)  
+       CreateGliders(30, 0, &amp;game.State)</code></pre><p>Let's run the game again.</p><pre><code><code>make</code></code></pre><p>Great!</p><p>We can see the addition of 4 gliders to a much larger window, and we can see them move across the screen as the generations increase.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xQHJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xQHJ!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!xQHJ!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!xQHJ!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!xQHJ!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xQHJ!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif" width="800" height="430" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:430,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!xQHJ!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!xQHJ!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!xQHJ!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!xQHJ!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8a65d47-eefe-4f5d-bb55-77500305bb02_800x430.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Scaled game state with gliders</figcaption></figure></div><p>Full code for this step is in the <a href="https://github.com/timlittle/blog-code/blob/4fa23147105c81cdf8a3848db0b90311f110468e/go-gameoflife/main.go">GitHub Repo</a></p><h4><strong>Setting up different simulations</strong></h4><p>Now that we have the game working, we can play around with different initial states to see how they evolve.</p><p>The other patterns are more complex compared to the glider, so I will create a helper function for adding the pattern, then define the patterns separately. More patterns can be found <a href="https://conwaylife.appspot.com/library/">here</a>.</p><pre><code>func addPattern(x, y int, pattern [][]int, gameState *[][]int) {  
 // Loop through the row  
 for row := range pattern {  
  
  // Loop through the  
  for col := 0; col &lt; len(pattern[row]); col++ {  
  
   // Update the game state if cell alive  
   if pattern[row][col] == 1 {  
    (*gameState)[y+row][x+col] = 1  
   }  
  }  
 }  
}</code></pre><pre><code>func CreateGliderGun(x, y int, gameState *[][]int) {  
 // Create a slice of the pattern  
 pattern := [][]int{  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},  
  {1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
 }  
 addPattern(x, y, pattern, gameState)
}</code></pre><pre><code>func CreatePulsar(x, y int, gameState *[][]int) {  
 // Create a slice of the pattern  
 pattern := [][]int{  
  {0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1},  
  {1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1},  
  {1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1},  
  {0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0},  
  {1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1},  
  {1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1},  
  {1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1},  
  {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},  
  {0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0},  
 }  
  
 addPattern(x, y, pattern, gameState)  
}</code></pre><pre><code>func CreatePentadecathlon(x, y int, gameState *[][]int) {  
 // Create a slice of the pattern  
 pattern := [][]int{  
  {0, 0, 1, 0, 0, 0, 0, 1, 0, 0},  
  {1, 1, 0, 1, 1, 1, 1, 0, 1, 1},  
  {0, 0, 1, 0, 0, 0, 0, 1, 0, 0},  
 }  
  
 addPattern(x, y, pattern, gameState)  
}</code></pre><p>Now we have our patterns, let's add them to the state.</p><pre><code>var game = NewGame(800, 400, 10)  
-       CreateGliders(0, 0, &amp;game.State)  
-       CreateGliders(10, 0, &amp;game.State)  
-       CreateGliders(20, 0, &amp;game.State)  
-       CreateGliders(30, 0, &amp;game.State)  
+       CreateGliderGun(0, 0, &amp;game.State)  
+       CreatePentadecathlon(40, 10, &amp;game.State)  
+       CreatePulsar(60, 20, &amp;game.State)  
  
        // Create the Raylib window using the state</code></pre><p>Run our game again.</p><pre><code><code>make</code></code></pre><p>Now our game is busy, with different interactions and patterns.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zHEr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zHEr!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zHEr!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif" width="800" height="430" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:430,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!zHEr!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 424w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 848w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1272w, https://substackcdn.com/image/fetch/$s_!zHEr!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee10ebeb-21a2-44b0-a051-ebf84677b6b0_800x430.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Full game of life</figcaption></figure></div><p>The full code can be found on the <a href="https://github.com/timlittle/blog-code/tree/main/go-gameoflife">GitHub repo</a></p><h4><strong>Testing</strong></h4><p>Something I have omitted from this tutorial is the testing. When developing this post, I created tests alongside the code to validate that the evolution rules and update function worked as expected. You can find my testing in the <a href="https://github.com/timlittle/blog-code/blob/main/go-gameoflife/main_test.go">main_test.go</a>.</p><p>The example, the code to test the IsCellAlive function, looks like:</p><pre><code>func TestIsCellAlive(t *testing.T) {  
 testCases := []struct {  
  name       string  
  want       int  
  current    int  
  neighbours int  
 }{  
  {name: "Live cell should not live with &lt;2", neighbours: 1, current: 1, want: 0},  
  {name: "Live cell should live with 2", neighbours: 2, current: 1, want: 1},  
  {name: "Live cells should live with 3", neighbours: 3, current: 1, want: 1},  
  {name: "Dead cells should live with 3", neighbours: 3, current: 0, want: 1},  
  {name: "Live cell should not live with &gt;3", neighbours: 4, current: 1, want: 0},  
  {name: "Dead cells should not live if already dead", neighbours: 0, want: 0},  
 }  
  
 for _, tt := range testCases {  
  t.Run(tt.name, func(t *testing.T) {  
   got := IsCellAlive(tt.current, tt.neighbours)  
  
   if got != tt.want {  
    t.Errorf("got %d, want %d", got, tt.want)  
   }  
  })  
 }  
}</code></pre><p>In future posts, I can cover testing more and potentially walk through how we could have done this with Test Driven Development (TDD)</p><h4><strong>Conclusion</strong></h4><p>We have walked through:</p><ul><li><p>Creating a window with raylib-go</p></li><li><p>Adding shapes to the window</p></li><li><p>Creating Conway&#8217;s Game of Life state</p></li><li><p>Updating the state with new generations</p></li><li><p>Adding new patterns to the state</p></li></ul><p>We just built Conway&#8217;s Game of Life in Go with raylib-go, starting from a blank window all the way to gliders, pulsars, and even a glider gun. Along the way, we covered rendering, updating state, and adding reusable patterns.</p><p>You can find the full source code in the <a href="https://github.com/timlittle/blog-code/blob/main/go-gameoflife/main.go">GitHub repo</a>. Try running it, tweak the patterns, or create your own.</p><p>If you build something cool with it, I&#8217;d love to see it! Share your experiments in the comments, or tag me on <a href="https://www.linkedin.com/in/timjlittle/">LinkedIn</a>.</p>]]></content:encoded></item><item><title><![CDATA[How to implement the Outbox pattern in Go and Postgres]]></title><description><![CDATA[How and why to use the Outbox pattern to build a robust event-driven system.]]></description><link>https://packagemain.tech/p/how-to-implement-the-outbox-pattern-in-golang</link><guid isPermaLink="false">https://packagemain.tech/p/how-to-implement-the-outbox-pattern-in-golang</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Tue, 16 Sep 2025 07:57:50 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/09503238-02cd-4d09-9b82-ed02d0f3a853_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div id="youtube2-hJ4S-5MirvU" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;hJ4S-5MirvU&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/hJ4S-5MirvU?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>I was at a <a href="https://www.containerdays.io/">ContainerDays</a> conference recently and attended a great talk from <a href="https://www.linkedin.com/in/nkuznetsov/">Nikolay Kuznetsov</a> about the Outbox pattern and resilient system design. It seemed like a powerful solution to a common problem in distributed systems, so I decided to dive deeper, and I want to share what I've learned and summarize for our readers in this post.</p><h3>The challenge in event-driven systems</h3><p>In modern, event-driven architectures, services often communicate asynchronously using a message broker. A typical flow looks like this: a service receives a request, updates its own database, and then publishes an event to notify other services about the change. Or these two actions happen in parallel.</p><p>Here's the problem: what happens if the database commit succeeds, but the subsequent call to the message broker fails? Maybe the broker is temporarily down, or there's a network glitch. Or what if the database is not available at that time? Or what if the program somehow crashes?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0TkS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0TkS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 424w, https://substackcdn.com/image/fetch/$s_!0TkS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 848w, https://substackcdn.com/image/fetch/$s_!0TkS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 1272w, https://substackcdn.com/image/fetch/$s_!0TkS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0TkS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png" width="1456" height="604" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:604,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:121954,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/173732554?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0TkS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 424w, https://substackcdn.com/image/fetch/$s_!0TkS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 848w, https://substackcdn.com/image/fetch/$s_!0TkS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 1272w, https://substackcdn.com/image/fetch/$s_!0TkS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d273d30-b9a1-42d7-b9b8-ef058160a0ca_2558x1062.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You end up in an inconsistent state. Your local database has the new data, but the rest of the system never gets the notification. This is a serious issue because the database operation and the message publishing are not <strong>atomic</strong> - they don't succeed or fail as a single, indivisible unit.</p><h3>Presenting the Outbox pattern</h3><p>This is where the Outbox pattern comes to the rescue! The core idea is simple but powerful: instead of directly publishing a message to the broker, you save the message to a dedicated "outbox" table within your local database. Crucially, you do this as part of the same database transaction as your business data changes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gtns!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gtns!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 424w, https://substackcdn.com/image/fetch/$s_!gtns!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 848w, https://substackcdn.com/image/fetch/$s_!gtns!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 1272w, https://substackcdn.com/image/fetch/$s_!gtns!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gtns!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png" width="1456" height="714" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:714,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:149289,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/173732554?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gtns!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 424w, https://substackcdn.com/image/fetch/$s_!gtns!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 848w, https://substackcdn.com/image/fetch/$s_!gtns!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 1272w, https://substackcdn.com/image/fetch/$s_!gtns!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3780b0c-aaeb-4919-b88c-0c241610e82e_2478x1216.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This approach leverages the atomicity of database transactions to guarantee that either both the business data is saved and the event is queued for sending, or neither is. The message can't get lost.</p><h3>A possible Outbox table schema</h3><p>To implement this, you need an outbox table in your database (or multiple ones). A typical schema might look something like this, storing the message content and its processing state.</p><pre><code><code>-- Example schema for a PostgreSQL outbox table
CREATE TABLE outbox (
    id uuid PRIMARY KEY,
    topic varchar(255) NOT NULL,
    message jsonb NOT NULL,
    state varchar(50) NOT NULL DEFAULT 'pending', -- e.g., pending, processed
    created_at timestamptz NOT NULL DEFAULT now(),
    processed_at timestamptz
);</code></code></pre><h3>The message relay</h3><p>Of course, just putting messages in a database table doesn't send them. You need a separate background process, often called a Message Dispatcher or Relay. This worker's job is to:</p><ul><li><p>Periodically query the outbox table for new, unprocessed messages.</p></li><li><p>Publish these messages to the actual message broker.</p></li><li><p>Once the broker confirms receipt, update the message's record in the outbox table to mark it as processed.</p></li><li><p>Handle errors and retries.</p></li></ul><p>This process guarantees <strong>at-least-once delivery</strong>. A message might be sent more than once if the relay publishes it but fails before it can mark the record as processed. Because of this, your message consumers should always be designed to be <strong>idempotent</strong>, meaning they can safely process the same message multiple times without causing issues.</p><h3>Minimal Go &amp; Postgres implementation</h3><p>Let's look at a concrete example using Go, <a href="https://github.com/jackc/pgx">pgx</a> for Postgres, and Google Cloud Pub/Sub. Imagine we have an <strong>orders</strong> service.</p><p>First, our business logic for creating an order will also create the outbox message within a single transaction.</p><pre><code><code>// orders/main.go

package main

import (
&#9;"context"
&#9;"encoding/json"
&#9;"log"
&#9;"os"

&#9;"github.com/google/uuid"
&#9;"github.com/jackc/pgx/v5"
&#9;"github.com/jackc/pgx/v5/pgxpool"
)

type Order struct {
&#9;ID       uuid.UUID `json:"id"`
&#9;Product  string    `json:"product"`
&#9;Quantity int       `json:"quantity"`
}

type OrderCreatedEvent struct {
&#9;OrderID uuid.UUID `json:"order_id"`
&#9;Product string    `json:"product"`
}

// createOrderInTx creates an order and its corresponding outbox event atomically.
func createOrderInTx(ctx context.Context, tx pgx.Tx, order Order) error {
&#9;// 1. Insert the order
&#9;_, err := tx.Exec(ctx, "INSERT INTO orders (id, product, quantity) VALUES ($1, $2, $3)",
&#9;&#9;order.ID, order.Product, order.Quantity)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;log.Printf("Inserted order %s into database", order.ID)

&#9;// 2. Prepare the event message for the outbox
&#9;event := OrderCreatedEvent{
&#9;&#9;OrderID: order.ID,
&#9;&#9;Product: order.Product,
&#9;}
&#9;msg, err := json.Marshal(event)
&#9;if err != nil {
&#9;&#9;return err
&#9;}

&#9;// 3. Insert the event into the outbox table
&#9;_, err = tx.Exec(ctx, "INSERT INTO outbox (topic, message) VALUES ($1, $2, $3)",
&#9;&#9;"orders.created", msg)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;log.Printf("Inserted outbox event for order %s", order.ID)

&#9;return nil
}

func main() {
&#9;ctx := context.Background()
&#9;pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
&#9;if err != nil {
&#9;&#9;log.Fatalf("Unable to connect to database: %v", err)
&#9;}
&#9;defer pool.Close()

&#9;tx, err := pool.Begin(ctx)
&#9;if err != nil {
&#9;&#9;log.Fatalf("Unable to begin transaction: %v", err)
&#9;}
&#9;defer tx.Rollback(ctx)

&#9;newOrder := Order{
&#9;&#9;ID:       uuid.New(),
&#9;&#9;Product:  "Super Widget",
&#9;&#9;Quantity: 10,
&#9;}

&#9;if err := createOrderInTx(ctx, tx, newOrder); err != nil {
&#9;&#9;log.Fatalf("Failed to create order: %v", err)
&#9;}

&#9;if err := tx.Commit(ctx); err != nil {
&#9;&#9;log.Fatalf("Failed to commit transaction: %v", err)
&#9;}

&#9;log.Println("Successfully created order and outbox event.")
}</code></code></pre><p>Next, the Relay process polls the database, sends the message, and updates the state. Using <code>FOR UPDATE SKIP LOCKED</code> is a great way to allow multiple relay instances to run concurrently without processing the same message.</p><pre><code><code>// relay/main.go
package main

import (
&#9;"context"
&#9;"log"
&#9;"time"

&#9;"cloud.google.com/go/pubsub"
&#9;"github.com/google/uuid"
&#9;"github.com/jackc/pgx/v5/pgxpool"
)

type OutboxMessage struct {
&#9;ID      uuid.UUID
&#9;Topic   string
&#9;Message []byte
}

func processOutboxMessages(ctx context.Context, pool *pgxpool.Pool, pubsubClient *pubsub.Client) error {
&#9;tx, err := pool.Begin(ctx)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;defer tx.Rollback(ctx)

&#9;// 1. Lock the next pending message so other relay instances don't grab it
&#9;rows, err := tx.Query(ctx, `
        SELECT id, topic, message
        FROM outbox
        WHERE state = 'pending'
        ORDER BY created_at
        LIMIT 1
        FOR UPDATE SKIP LOCKED
    `)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;defer rows.Close()

&#9;// 2. If we found a message, publish it to Pub/Sub
&#9;var msg OutboxMessage
&#9;if rows.Next() {
&#9;&#9;if err := rows.Scan(&amp;msg.ID, &amp;msg.Topic, &amp;msg.Message); err != nil {
&#9;&#9;&#9;return err
&#9;&#9;}
&#9;} else {
&#9;&#9;// No new messages
&#9;&#9;return nil
&#9;}

&#9;log.Printf("Publishing message %s to topic %s", msg.ID, msg.Topic)
&#9;result := pubsubClient.Topic(msg.Topic).Publish(ctx, &amp;pubsub.Message{
&#9;&#9;Data: msg.Message,
&#9;})
&#9;_, err = result.Get(ctx)
&#9;if err != nil {
&#9;&#9;return err
&#9;}

&#9;// 3. Mark the message as processed
&#9;_, err = tx.Exec(ctx, "UPDATE outbox SET state = 'processed', processed_at = now() WHERE id = $1", msg.ID)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;log.Printf("Marked message %s as processed", msg.ID)

&#9;return tx.Commit(ctx)
}

func main() {
&#9;// TODO: initialize actual Postgres and Pubsub connections
&#9;var (
&#9;&#9;pool         *pgxpool.Pool
&#9;&#9;pubsubClient *pubsub.Client
&#9;)

&#9;// feel free to use another interval
&#9;ticker := time.NewTicker(1 * time.Second)
&#9;defer ticker.Stop()
&#9;for range ticker.C {
&#9;&#9;if err := processOutboxMessages(context.Background(), pool, pubsubClient); err != nil {
&#9;&#9;&#9;log.Printf("Error processing outbox: %v", err)
&#9;&#9;}
&#9;}
}
</code></code></pre><p>Why then we say that the message is sent at least once? That's because of the rare case when the message is sent, but something happened when updating the message status in the database, for example due to a service crash.</p><h3>Another alternative: Postgres logical replication</h3><p>We touched this topic briefly in the <a href="https://packagemain.tech/p/real-time-database-change-tracking">following post</a>.</p><p>While polling is a simple and effective strategy, it can introduce some latency and be resource-intensive. For those using PostgreSQL, there's a more advanced, push-based alternative: logical replication.</p><p>Databases like Postgres maintain a <strong>Write-Ahead Log (WAL)</strong>, which is an append-only log of every change made to the database. Logical replication allows you to tap into this log and stream changes for specific tables as they happen.</p><p>Instead of your relay constantly asking the database "Anything new?", the database effectively tells your relay, "Hey, a new row was just inserted into the outbox table!". This can be more efficient and provide lower latency, though it adds some implementation complexity.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eXIM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eXIM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 424w, https://substackcdn.com/image/fetch/$s_!eXIM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 848w, https://substackcdn.com/image/fetch/$s_!eXIM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 1272w, https://substackcdn.com/image/fetch/$s_!eXIM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eXIM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png" width="1456" height="755" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:755,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:124267,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/173732554?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eXIM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 424w, https://substackcdn.com/image/fetch/$s_!eXIM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 848w, https://substackcdn.com/image/fetch/$s_!eXIM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 1272w, https://substackcdn.com/image/fetch/$s_!eXIM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7cd41d1-7535-497a-91e9-cfeb771eb79f_2362x1224.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In Go you can use <a href="https://github.com/jackc/pglogrepl">pglogrepl</a> which is a Go library designed for interacting with PostgreSQL&#8217;s logical replication protocol.</p><h3>Conclusion</h3><p>To summarize, the Outbox Pattern ensures that a message was sent (e.g. to a queue) successfully at least once. With this pattern, instead of directly publishing a message to the queue, we store it in temporary outbox table first. We're wrapping the entity save and message storing in a single transaction so this operation is atomic. It will be published later through a background process called Message Relay.</p><p>While the concept is simple, it can be complicated to implement the actual solution and there multiple ways of doing so.</p><h3>Resources</h3><ul><li><p><a href="https://github.com/plutov/packagemain/tree/master/outbox">Source code</a></p></li><li><p><a href="https://www.postgresql.org/docs/current/wal-intro.html">Write-Ahead Logging (WAL)</a></p></li><li><p><a href="https://github.com/jackc/pglogrepl">pglogrepl</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[The evolution of code review practices in the world of AI]]></title><description><![CDATA[Code reviews are being redefined by AI, and I think it's taking a good direction. Let's review some of the developments in this area, and tools like CodeRabbit.]]></description><link>https://packagemain.tech/p/evolution-of-code-review-practices-code-rabbit</link><guid isPermaLink="false">https://packagemain.tech/p/evolution-of-code-review-practices-code-rabbit</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Tue, 22 Jul 2025 11:54:03 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/096ae803-c5d4-4565-8504-c16d298efbb4_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The world of software development is abuzz with AI's transformative power, largely focusing on its ability to generate code. However, coding is only a part of the intricate software development process, and while everyone is arguing about vibe coding, or hopping between endlessly new emerging VS Code forks, LLMs are making a very good adoption in few other areas, for example <strong>code reviews</strong>.</p><p>I don&#8217;t know about you, but I love code reviews.</p><h3>The power of code reviews</h3><p>There are different ways to conduct a code review (mailing lists, pull/merge requests, even the code printouts), but they all serve the same main purposes.</p><ul><li><p>&#128027; Help catch bugs early, which can save time and resources later in the development cycle.</p></li><li><p>&#9989; Help maintain high code quality and adhere to the code standards, performance considerations, etc.</p></li><li><p>&#128172; Code reviews can improve team communication. They create a platform for discussing design decisions and coding standards. This dialogue can lead to better solutions and a more cohesive team.</p></li><li><p>&#128218; Promote knowledge sharing among team members. When developers review each other's code, they learn different approaches and techniques, which can enhance their skills.</p></li></ul><p>Many would say that you need a code review process in teams only, but I would argue and say that it&#8217;s also helpful when you&#8217;re working on a project alone, mainly for the documentation purpose and self-review.</p><p>But code reviews are time consuming, reviewing code manually takes hours, slowing down development. As well as there are other challenges. For example different reviewers have different standards, leading to inconsistent feedback. Reviewers may overlook issues due to familiarity with the project. And as teams grow, manual review processes become bottlenecks.</p><p>I have a good quote for you about the inconsistencies, which is unfortunately very true very often:</p><div class="pullquote"><p>Give a developer a 10-line Pull Request and they'll find 10 issues. Give them a 500-line one and they'll just say, "LGTM" :)</p></div><h3>AI-powered code reviews</h3><p>And LLMs can help with these issues quite well, and can be a great companion to software developers. I say &#8220;companion&#8220; because LLMs excel in some routine parts, while humans excel in software design, architecture decisions, they have taste and know everything about the software system. And that&#8217;s great, let&#8217;s outsource routine boring tasks to LLMs to have more time for creativity.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Lbgo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Lbgo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 424w, https://substackcdn.com/image/fetch/$s_!Lbgo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 848w, https://substackcdn.com/image/fetch/$s_!Lbgo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 1272w, https://substackcdn.com/image/fetch/$s_!Lbgo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Lbgo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png" width="1456" height="808" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:808,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:206534,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/167572973?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Lbgo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 424w, https://substackcdn.com/image/fetch/$s_!Lbgo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 848w, https://substackcdn.com/image/fetch/$s_!Lbgo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 1272w, https://substackcdn.com/image/fetch/$s_!Lbgo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd4fdf5-30fb-49a1-913d-49ebe253b670_2073x1150.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There is probably a world where these 2 circles intersect a bit, as there is no black and white.</p><p>Let&#8217;s move closer to the tools, it&#8217;s extremely easy to integrate such solutions into an existing system and workflows, as they don&#8217;t require teams to radically change the way they work. So far I used only <a href="https://github.com/features/copilot">GitHub Copilot</a> and <a href="https://coderabbit.link/alex">CodeRabbit</a>, and both were really easy to setup in the GitHub. But I would say that I find CodeRabbit settings more advanced, as you can fine tune everything on per-repository level.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NnJz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NnJz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 424w, https://substackcdn.com/image/fetch/$s_!NnJz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 848w, https://substackcdn.com/image/fetch/$s_!NnJz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 1272w, https://substackcdn.com/image/fetch/$s_!NnJz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NnJz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png" width="512" height="578.4427480916031" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1184,&quot;width&quot;:1048,&quot;resizeWidth&quot;:512,&quot;bytes&quot;:178313,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/167572973?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NnJz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 424w, https://substackcdn.com/image/fetch/$s_!NnJz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 848w, https://substackcdn.com/image/fetch/$s_!NnJz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 1272w, https://substackcdn.com/image/fetch/$s_!NnJz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7dc0b2c-3d50-43b6-a7d9-88b598e39f34_1048x1184.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After the integration is ready, these tools start automatically reviewing your pull requests and enhancing them with additional info, such as summary, comments, suggestions.</p><div><hr></div><p><em>Disclaimer: I am a big fan of CodeRabbit, and am grateful they partnered on this article. However, I will try to only write my honest opinion on this topic of code review practices using AI and tools like CodeRabbit.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://coderabbit.link/alex&quot;,&quot;text&quot;:&quot;Get Started Today&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://coderabbit.link/alex"><span>Get Started Today</span></a></p><div><hr></div><h3>Team vs Solo</h3><p>Likely, in a team, there are already processes in place that involve some code review step. It could be a slow and blocking phase. Developers need to switch context to review someone else&#8217;s code, then spend a lot of time writing the summary of their own PR. I honestly remember days when half of my day was about reviewing the code, I am sure it would be much faster nowadays was I using something like CodeRabbit.</p><p>But I also wanted to talk about the projects where there is no clear concept of a &#8220;team&#8221; (for example an OSS project), or it&#8217;s your own solo project.</p><p>For example, as a maintainer of an open source software, you may receive some pull request and spend a lot of your free time understanding what the change is about, then ditching obvious errors, etc.</p><p>And exactly here I see a great value of tools like CodeRabbit.</p><h3>Real example</h3><p>Let&#8217;s take an example from a real project I&#8217;m maintaining - <a href="https://github.com/plutov/paypal">plutov/paypal</a>. Few days ago I received a new <a href="https://github.com/plutov/paypal/pull/289">PR</a> from a contributor and when I opened it, it had a nice summary:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yxTR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yxTR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 424w, https://substackcdn.com/image/fetch/$s_!yxTR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 848w, https://substackcdn.com/image/fetch/$s_!yxTR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 1272w, https://substackcdn.com/image/fetch/$s_!yxTR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yxTR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png" width="588" height="398.59615384615387" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:987,&quot;width&quot;:1456,&quot;resizeWidth&quot;:588,&quot;bytes&quot;:191193,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/167572973?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yxTR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 424w, https://substackcdn.com/image/fetch/$s_!yxTR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 848w, https://substackcdn.com/image/fetch/$s_!yxTR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 1272w, https://substackcdn.com/image/fetch/$s_!yxTR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98954727-f116-41b6-a35f-58b0a8fcbc92_1490x1010.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That helped both me and the author of a PR. On <a href="https://github.com/plutov/formulosity/pull/46">another PR</a> CodeRabbit was able to generate a sequence diagram to explain the database comms, isn&#8217;t that cool?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_wXQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_wXQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 424w, https://substackcdn.com/image/fetch/$s_!_wXQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 848w, https://substackcdn.com/image/fetch/$s_!_wXQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 1272w, https://substackcdn.com/image/fetch/$s_!_wXQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_wXQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png" width="1456" height="707" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:707,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:99035,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/167572973?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_wXQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 424w, https://substackcdn.com/image/fetch/$s_!_wXQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 848w, https://substackcdn.com/image/fetch/$s_!_wXQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 1272w, https://substackcdn.com/image/fetch/$s_!_wXQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1f8331e-2ea5-4cae-bc28-67cee9f252a9_1602x778.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And you can also work with <a href="https://docs.coderabbit.ai/guides/use-vscode/">CodeRabbit directly in your VSCode</a> (no Neovim yet unfortunately).</p><h3>Conclusion</h3><p>In conclusion, the integration of AI into the code review process isn't about replacing humans, but rather augmenting them by automating routine tasks and enabling engineers to concentrate on higher-order concerns like architecture, business logic, scalability.</p><p>And if you want to learn more about CodeRabbit, check the link below, it&#8217;s free.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://coderabbit.link/alex&quot;,&quot;text&quot;:&quot;Get Started Today&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://coderabbit.link/alex"><span>Get Started Today</span></a></p>]]></content:encoded></item><item><title><![CDATA[Redis streams: a different take on event-driven]]></title><description><![CDATA[A different take on event driven.]]></description><link>https://packagemain.tech/p/redis-streams-event-driven</link><guid isPermaLink="false">https://packagemain.tech/p/redis-streams-event-driven</guid><dc:creator><![CDATA[Julien Singler]]></dc:creator><pubDate>Mon, 14 Jul 2025 16:00:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6HF2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Redis Streams are a powerful data structure in Redis designed for handling real-time, high-throughput event data. They combine the simplicity of an append-only log with advanced features for event consumption and processing, making them ideal for use cases like event sourcing, sensor data collection, and notification systems.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6HF2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6HF2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 424w, https://substackcdn.com/image/fetch/$s_!6HF2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 848w, https://substackcdn.com/image/fetch/$s_!6HF2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!6HF2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6HF2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg" width="860" height="573" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:573,&quot;width&quot;:860,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:137951,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/167598204?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6HF2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 424w, https://substackcdn.com/image/fetch/$s_!6HF2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 848w, https://substackcdn.com/image/fetch/$s_!6HF2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!6HF2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F814e951a-3665-42ef-854d-228c8d430fa7_860x573.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>What are Redis Streams ?</h2><p>A <strong>Redis stream</strong> acts as an append-only log where each new entry is assigned a unique, time-based ID. Unlike traditional logs, Redis Streams support random access in O(1) time and enable complex consumption patterns, such as consumer groups for distributed processing. This makes them suitable for scenarios where you need to record, syndicate, and process events in real time.</p><p><strong>Key characteristics:</strong></p><ul><li><p><strong>Append-only:</strong> Once data is written, it cannot be modified.</p></li><li><p><strong>Immutable entries:</strong> Each entry is a set of key-value pairs with a unique ID.</p></li><li><p><strong>Real-time syndication:</strong> Multiple consumers can process events as they arrive.</p></li><li><p><strong>Trimming and retention:</strong> Streams can be trimmed to prevent unbounded growth.</p></li></ul><div><hr></div><h3>Cut Code Review Time &amp; Bugs in Half (Sponsor)</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tbzq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:399652,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Code reviews are critical but time-consuming. <a href="https://coderabbit.link/alex">CodeRabbit</a> acts as your AI co-pilot, providing instant Code review comments and potential impacts of every pull request. </p><p>Beyond just flagging issues, CodeRabbit provides one-click fix suggestions and lets you define custom code quality rules using AST Grep patterns, catching subtle issues that traditional static analysis tools might miss.</p><p>CodeRabbit has so far reviewed more than 10 million PRs, installed on 1 million repositories, and used by 70 thousand Open-source projects. CodeRabbit is free for all open-source repo's!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://coderabbit.link/alex&quot;,&quot;text&quot;:&quot;Get Started Today&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://coderabbit.link/alex"><span>Get Started Today</span></a></p><div><hr></div><h2>How to use</h2><h4><em>Single client </em></h4><p>For basic usage, you interact with Redis Streams using a set of core commands:</p><ul><li><p><strong>XADD:</strong> Add a new entry to a stream. Each entry consists of field-value pairs, and Redis generates a unique ID if you use the <code>*</code> wildcard.</p></li><li><p><strong>XRANGE:</strong> Retrieve a range of entries by specifying start and end IDs, making it easy to access historical data or iterate over the stream.</p></li><li><p><strong>XLEN:</strong> Get the number of entries in a stream&#8212;useful for monitoring or batch processing.</p></li></ul><p><strong>Example workflow:</strong></p><pre><code>XADD mystream * field1 value1 field2 value2
XLEN mystream
XRANGE mystream - +</code></pre><p>The <code>-</code> and <code>+</code> special IDs mean respectively the minimum ID possible and the maximum ID possible inside a stream, so the XRANGE command will just return every entry in the stream.</p><h4><em>Consumer groups</em></h4><p>For scalable processing, Redis Streams support <strong>consumer groups</strong>. This allows multiple consumers to cooperatively process messages from the same stream, with Redis distributing entries among them.</p><p><strong>How it works:</strong></p><ul><li><p><strong>Consumer group:</strong> A named group coordinating multiple consumers.</p></li><li><p><strong>Consumers:</strong> Each consumer reads a subset of entries, ensuring parallelism.</p></li><li><p><strong>Acknowledgement:</strong> Consumers must acknowledge (<code>XACK</code>) processed entries. Acknowledgements are tied to a consumer group and its consumers.</p></li></ul><p><strong>Example workflow:</strong></p><pre><code>XGROUP CREATE mystream mygroup $
XREADGROUP GROUP mygroup consumer1 STREAMS mystream &gt;
XACK mystream mygroup &lt;entry-id&gt;</code></pre><h2>Handling failures and message replays</h2><p>One of the strengths of Redis Streams is the ability to <strong>replay messages</strong> in case of incidents or failures. Since messages are not deleted automatically after acknowledgment, you can reprocess them if needed.</p><p><strong>Auto-claiming and message management:</strong></p><ul><li><p><strong>XAUTOCLAIM:</strong> Automatically transfer ownership of pending messages that have been idle for too long to another consumer, ensuring that no message is left unprocessed if a consumer crashes.</p></li><li><p><strong>Manual deletion:</strong> Messages are not deleted upon acknowledgment; you must use <code>XDEL</code> to remove them. This gives you control but also requires you to manage retention, typically after all consumer groups have processed the message.</p></li></ul><p><strong>Trade-offs:</strong></p><ul><li><p><strong>Manual cleanup:</strong> You need to implement logic to delete messages once they're no longer needed, as Redis does not do this automatically even if all consumer groups have acknowledged a message.</p></li><li><p><strong>Replay capability:</strong> The upside is that you can replay messages without recreating payloads, simply by reading from the stream history as needed.</p></li></ul><h2>Practical considerations</h2><ul><li><p><strong>Memory management:</strong> Use <code>XTRIM</code> or the <code>MAXLEN</code> option with <code>XADD</code> to cap stream size and prevent unbounded memory growth.</p></li><li><p><strong>Visibility:</strong> Inspect pending messages with <code>XPENDING</code> to monitor which messages are yet to be processed or acknowledged by consumers.</p></li></ul><p>Redis Streams provide a flexible, high-performance foundation for event-driven architectures. By leveraging consumer groups, auto-claiming, and careful retention management, you can build scalable, resilient systems that can recover from failures and replay events as needed, all with the speed and simplicity Redis is known for.</p><p>Also, it&#8217;s nice to see Redis to be open-source (AGPL) again.</p><h3>Cut Code Review Time &amp; Bugs in Half (Sponsor)</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tbzq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:399652,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Code reviews are critical but time-consuming. <a href="https://coderabbit.link/alex">CodeRabbit</a> acts as your AI co-pilot, providing instant Code review comments and potential impacts of every pull request. </p><p>Beyond just flagging issues, CodeRabbit provides one-click fix suggestions and lets you define custom code quality rules using AST Grep patterns, catching subtle issues that traditional static analysis tools might miss.</p><p>CodeRabbit has so far reviewed more than 10 million PRs, installed on 1 million repositories, and used by 70 thousand Open-source projects. CodeRabbit is free for all open-source repo's!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://coderabbit.link/alex&quot;,&quot;text&quot;:&quot;Get Started Today&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://coderabbit.link/alex"><span>Get Started Today</span></a></p><h2><br></h2>]]></content:encoded></item><item><title><![CDATA[Developing a terminal UI in Go with Bubble Tea]]></title><description><![CDATA[Developing CLIs and TUIs in Go is fun, and there are really good packages out there to make it so. And when it comes to terminal apps, there is an amazing library called Bubble Tea to build beautiful]]></description><link>https://packagemain.tech/p/terminal-ui-bubble-tea</link><guid isPermaLink="false">https://packagemain.tech/p/terminal-ui-bubble-tea</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Mon, 07 Jul 2025 16:04:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/dfdb1930-d11a-4249-8d5e-bab957387a60_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Many developers love using command line tools for daily development tasks, at least I do. It's fun, they are usually performant. For example there are many Desktop applications to work with git, though I believe that the majority of programmers love to use git CLI (or TUIs like <a href="https://github.com/jesseduffield/lazygit">lazygit</a>) as it's faster and can be automated, and is the same on all environments: be it your dev machine or server. There are 2 main flavours of CLIs: one is imperative, command-based CLIs, such as git again, or kubectl, where you define everything in prompt using sub-commands and flags. And the second is so-called Terminal Apps or Terminal UI (commonly called TUI) which is more interactive, and is basically a whole application that runs in your terminal.</p><p>For example, there is a <a href="https://github.com/bensadeh/circumflex">circumflex</a> TUI that lets you read Hacker News from your Terminal.</p><pre><code>clx</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3sWL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3sWL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 424w, https://substackcdn.com/image/fetch/$s_!3sWL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 848w, https://substackcdn.com/image/fetch/$s_!3sWL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 1272w, https://substackcdn.com/image/fetch/$s_!3sWL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3sWL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png" width="1456" height="820" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e166c350-c15a-4493-9712-8febb39d6101_2670x1504.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:820,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:407743,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3sWL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 424w, https://substackcdn.com/image/fetch/$s_!3sWL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 848w, https://substackcdn.com/image/fetch/$s_!3sWL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 1272w, https://substackcdn.com/image/fetch/$s_!3sWL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe166c350-c15a-4493-9712-8febb39d6101_2670x1504.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And there are more examples like that, check out <a href="https://packagemain.tech/p/essential-clitui-tools-for-developers">this article</a> that lists just few.</p><p>Developing CLIs and TUIs in Go is fun, and there are really good packages out there to make it so. For example, for developing the command-based imperative CLIs you can use <a href="https://cobra.dev/">Cobra</a> library from <a href="https://spf13.com/">Steve Francia</a>, which was used to build popular kubectl, hugo and github CLIs.</p><p>And when it comes to terminal apps, there is an amazing library called <a href="https://github.com/charmbracelet/bubbletea">Bubble Tea</a> to build beautiful interactive TUIs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qbKH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qbKH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 424w, https://substackcdn.com/image/fetch/$s_!qbKH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 848w, https://substackcdn.com/image/fetch/$s_!qbKH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 1272w, https://substackcdn.com/image/fetch/$s_!qbKH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qbKH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png" width="625" height="415" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:415,&quot;width&quot;:625,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35047,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qbKH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 424w, https://substackcdn.com/image/fetch/$s_!qbKH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 848w, https://substackcdn.com/image/fetch/$s_!qbKH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 1272w, https://substackcdn.com/image/fetch/$s_!qbKH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3fb01da6-f797-47e0-9200-925d26a2ed0c_625x415.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For example the <a href="https://github.com/bensadeh/circumflex">circumflex</a> TUI that we've seen before was developed using Bubble Tea.</p><div><hr></div><h3>Cut Code Review Time &amp; Bugs in Half (Sponsor)</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tbzq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:399652,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Code reviews are critical but time-consuming. <a href="https://coderabbit.link/alex">CodeRabbit</a> acts as your AI co-pilot, providing instant Code review comments and potential impacts of every pull request. </p><p>Beyond just flagging issues, CodeRabbit provides one-click fix suggestions and lets you define custom code quality rules using AST Grep patterns, catching subtle issues that traditional static analysis tools might miss.</p><p>CodeRabbit has so far reviewed more than 10 million PRs, installed on 1 million repositories, and used by 70 thousand Open-source projects. CodeRabbit is free for all open-source repo's!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://coderabbit.link/alex&quot;,&quot;text&quot;:&quot;Get Started Today&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://coderabbit.link/alex"><span>Get Started Today</span></a></p><div><hr></div><h3>Note-taking TUI</h3><p>In this article we will build a terminal-based note taking app using Bubble Tea and some other libraries. It will be a very simple application with SQLite store for storing the notes.</p><h3>Installation</h3><p>If you don&#8217;t have Bubble Tea installed yet, run the following commands to install it as well as few other adjacent packages :</p><pre><code>go get github.com/charmbracelet/bubbletea
go get github.com/charmbracelet/lipgloss
go get github.com/charmbracelet/bubbles</code></pre><p>Bubble Tea is usually used with other libraries, as you can see above, we installed them too.</p><p><a href="https://github.com/charmbracelet/lipgloss">lipgloss</a> is a great styling library from Charm.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NbgU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NbgU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 424w, https://substackcdn.com/image/fetch/$s_!NbgU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 848w, https://substackcdn.com/image/fetch/$s_!NbgU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 1272w, https://substackcdn.com/image/fetch/$s_!NbgU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NbgU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png" width="1456" height="1454" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1454,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:659547,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NbgU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 424w, https://substackcdn.com/image/fetch/$s_!NbgU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 848w, https://substackcdn.com/image/fetch/$s_!NbgU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 1272w, https://substackcdn.com/image/fetch/$s_!NbgU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6756aaf-f080-4b53-989a-ef3ae163ee41_2650x2646.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And <a href="https://github.com/charmbracelet/bubbles">bubbles</a><code> is a TUI components library for Bubble Tea (also from Charm). For example File picker:</code></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u9WU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u9WU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 424w, https://substackcdn.com/image/fetch/$s_!u9WU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 848w, https://substackcdn.com/image/fetch/$s_!u9WU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 1272w, https://substackcdn.com/image/fetch/$s_!u9WU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u9WU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif" width="1200" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:600,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:170697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!u9WU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 424w, https://substackcdn.com/image/fetch/$s_!u9WU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 848w, https://substackcdn.com/image/fetch/$s_!u9WU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 1272w, https://substackcdn.com/image/fetch/$s_!u9WU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a2e24a4-16bb-4907-a370-16fdd0fb9b08_1200x600.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Bubble Tea Architecture</h3><p>Before jumping to our program, let&#8217;s review how Bubble Tea actually works.</p><p>Bubble Tea is based on the functional design paradigms of <a href="https://guide.elm-lang.org/architecture/">The Elm Architecture</a>, which happens to work nicely with Go. It's a delightful way to build applications.</p><p>Bubble Tea programs are comprised of a&nbsp;model&nbsp;that describes the application state and three simple methods on that model:</p><ul><li><p><strong>Init</strong>, a function that returns an initial command for the application to run.</p></li><li><p><strong>Update</strong>, a function that handles incoming events and updates the model accordingly.</p></li><li><p><strong>View</strong>, a function that renders the UI based on the data in the model.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-Z_1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-Z_1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 424w, https://substackcdn.com/image/fetch/$s_!-Z_1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 848w, https://substackcdn.com/image/fetch/$s_!-Z_1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 1272w, https://substackcdn.com/image/fetch/$s_!-Z_1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-Z_1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png" width="596" height="429.4581560283688" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1016,&quot;width&quot;:1410,&quot;resizeWidth&quot;:596,&quot;bytes&quot;:80982,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-Z_1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 424w, https://substackcdn.com/image/fetch/$s_!-Z_1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 848w, https://substackcdn.com/image/fetch/$s_!-Z_1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 1272w, https://substackcdn.com/image/fetch/$s_!-Z_1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64ef01e8-05b0-4d3e-8a3e-78ec5b0a7ec6_1410x1016.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now, how does it translate to code?</p><h3>Model</h3><p>A good place to start is the Model. Our note-taking application can be in multiple states (list view, edit title view, edit body view), and as you can imagine we will need UI elements such as text input, textarea, list. What&#8217;s cool is that our main model can contain other models as well, so in a way we&#8217;re building a tree of models.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UCEt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UCEt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 424w, https://substackcdn.com/image/fetch/$s_!UCEt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 848w, https://substackcdn.com/image/fetch/$s_!UCEt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 1272w, https://substackcdn.com/image/fetch/$s_!UCEt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UCEt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png" width="1456" height="1076" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1076,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:86286,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UCEt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 424w, https://substackcdn.com/image/fetch/$s_!UCEt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 848w, https://substackcdn.com/image/fetch/$s_!UCEt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 1272w, https://substackcdn.com/image/fetch/$s_!UCEt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bb8ad6-5ff6-450e-9b95-2c1dbd584df5_1572x1162.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So here is how our model may look like:</p><p><a href="https://github.com/plutov/packagemain/blob/master/31-bubbletea/model.go">model.go</a></p><pre><code>package main

import (
&#9;"log"

&#9;"github.com/charmbracelet/bubbles/textarea"
&#9;"github.com/charmbracelet/bubbles/textinput"
&#9;tea "github.com/charmbracelet/bubbletea"
)

const (
&#9;listView uint = iota
&#9;titleView
&#9;bodyView
)

type model struct {
&#9;store     *Store
&#9;state     uint
&#9;textarea  textarea.Model
&#9;textinput textinput.Model
&#9;currNote  Note
&#9;notes     []Note
&#9;listIndex int
}

func NewModel(store *Store) model {
&#9;notes, err := store.GetNotes()
&#9;if err != nil {
&#9;&#9;log.Fatalf("unable to get notes: %v", err)
&#9;}

&#9;return model{
&#9;&#9;store:     store,
&#9;&#9;state:     listView,
&#9;&#9;textarea:  textarea.New(),
&#9;&#9;textinput: textinput.New(),
&#9;&#9;notes:     notes,
&#9;}
}

func (m model) Init() tea.Cmd {
&#9;return nil
}</code></pre><p>As you can see our model is basically a state of our application. It also knows about the notes storage (sqlite in my case, but we will not focus on much on that). <strong>NewModel()</strong> function creates a new fresh state, and <strong>Init()</strong> is empty in our case, as initial command for the application is not required and not needed in our case.</p><h3>Main Loop</h3><p>With the model in place we can initiate a Bubble Tea program in <a href="https://github.com/plutov/packagemain/blob/master/31-bubbletea/main.go">main.go</a></p><pre><code>package main

import (
&#9;"log"

&#9;tea "github.com/charmbracelet/bubbletea"
)

func main() {
&#9;store := new(Store)
&#9;if err := store.Init(); err != nil {
&#9;&#9;log.Fatalf("unable to init store: %v", err)
&#9;}

&#9;m := NewModel(store)

&#9;p := tea.NewProgram(m)
&#9;if _, err := p.Run(); err != nil {
&#9;&#9;log.Fatalf("unable to run tui: %v", err)
&#9;}
}</code></pre><p>As you can see, we can pass our model to <strong>tea.NewProgram()</strong> and Bubble Tea will do the rest for us, assuming that our Model implements the interface with <strong>Init(), Update(), View()</strong> methods.</p><pre><code>type Model interface {
    Init() Cmd
    Update(msg Msg) (Model, Cmd)
    View() string
}</code></pre><h3>Update</h3><p>The <strong>Update()</strong> method handles user input (or any other events such as Ticks for example).</p><p>Here we can react to various events and update our model accordingly. For example, when a user presses the hotkeys, we switch to another view.</p><p>What&#8217;s interesting is that our Model contains other models so we must propagate the <strong>Update()</strong> accordingly.</p><pre><code>func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
&#9;m.textarea, _ = m.textarea.Update(msg)
&#9;m.textinput, _ = m.textinput.Update(msg)

&#9;switch msg := msg.(type) {
&#9;// handle key strokes
&#9;case tea.KeyMsg:
&#9;&#9;key := msg.String()

&#9;&#9;switch m.state {
&#9;&#9;// List View key bindings
&#9;&#9;case listView:
&#9;&#9;&#9;switch key {
&#9;&#9;&#9;case "q":
&#9;&#9;&#9;&#9;return m, tea.Quit
&#9;&#9;&#9;case "n":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;case "up", "k":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;case "down", "j":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;case "enter":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;}

&#9;&#9;// Title Input View key bindings
&#9;&#9;case titleView:
&#9;&#9;&#9;switch key {
&#9;&#9;&#9;case "enter":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;case "esc":
&#9;&#9;&#9;&#9;m.state = listView
&#9;&#9;&#9;}

&#9;&#9;// Body Textarea key bindings
&#9;&#9;case bodyView:
&#9;&#9;&#9;switch key {
&#9;&#9;&#9;case "ctrl+s":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;case "esc":
&#9;&#9;&#9;&#9;// ...
&#9;&#9;&#9;}
&#9;&#9;}
&#9;}

&#9;return m, nil
}</code></pre><p>As you can see here in our <strong>Update()</strong> function we react to the following keystrokes:</p><ul><li><p>q - quit the app</p></li><li><p>n - new note</p></li><li><p>j,k - move up,down between notes</p></li><li><p>enter - open note</p></li><li><p>ctrl+s - save note</p></li><li><p>esc - exit the step</p></li></ul><p>The <strong>Msg</strong> type in Bubble Tea is flexible and can carry various data. In this scenario, it resembles browser events in JavaScript. For instance, a timer event might not carry data, while a click event specifies what was clicked.</p><p>But be aware, that messages are not necessarily received in the order they are sent, in Go, if you have more than one go routine sending to a channel, the order in which the sends and receives occur is unspecified.</p><h3>View</h3><p>The View method is where we take the state of our Model and render it. Here we will be using the libraries <strong>libgloss</strong> and <strong>bubbles</strong> that we installed previously.</p><p><a href="https://github.com/plutov/packagemain/blob/master/31-bubbletea/view.go">view.go</a></p><pre><code>package main

import (
&#9;"strings"

&#9;"github.com/charmbracelet/lipgloss"
)

var (
&#9;appNameStyle = lipgloss.NewStyle().Background(lipgloss.Color("99")).Padding(0, 1)

&#9;faint = lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Faint(true)

&#9;listEnumeratorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
)

func (m model) View() string {
&#9;s := appNameStyle.Render("NOTES APP") + "\n\n"

&#9;if m.state == titleView {
&#9;&#9;s += "Note title:\n\n"
&#9;&#9;s += m.textinput.View() + "\n\n"
&#9;&#9;s += faint.Render("enter - save &#8226; esc - discard")
&#9;}

&#9;if m.state == bodyView {
&#9;&#9;s += "Note:\n\n"
&#9;&#9;s += m.textarea.View() + "\n\n"
&#9;&#9;s += faint.Render("ctrl+s - save &#8226; esc - discard")
&#9;}

&#9;if m.state == listView {
&#9;&#9;for i, n := range m.notes {
&#9;&#9;&#9;// render each note
&#9;&#9;}
&#9;&#9;s += faint.Render("n - new note &#8226; q - quit")
&#9;}

&#9;return s
}</code></pre><p>Again, as we have other models like textarea and textinput, we call View() on them too to embed them into our top-level view. Also, because the View() method returns a simple string, it becomes easy to test this deterministic function.</p><h3>Conclusion</h3><p>I didn&#8217;t include all the code, like SQLite storage and some utils, but you can find the <a href="https://github.com/plutov/packagemain/tree/master/31-bubbletea">full program here</a> and even run it yourself, all you need is Go installed.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oTFb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oTFb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 424w, https://substackcdn.com/image/fetch/$s_!oTFb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 848w, https://substackcdn.com/image/fetch/$s_!oTFb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 1272w, https://substackcdn.com/image/fetch/$s_!oTFb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oTFb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png" width="1224" height="426" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:426,&quot;width&quot;:1224,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40283,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oTFb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 424w, https://substackcdn.com/image/fetch/$s_!oTFb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 848w, https://substackcdn.com/image/fetch/$s_!oTFb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 1272w, https://substackcdn.com/image/fetch/$s_!oTFb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b6a0e24-4c71-4403-be80-33b59d767b14_1224x426.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Building interactive TUIs in Go is genuinely enjoyable, thanks to powerful libraries like Bubble Tea.</p><p>To me the main strength of Bubble Tea is its simplicity with Elm Architecture, allowing developers to craft robust and responsive TUI applications, but also very modular.</p><p>The applications developed using Bubble Tea are usually very performant as well.</p><p>Let me know in the comments below if you built anything fun with Bubble Tea. I for example built this simple <a href="https://github.com/plutov/ultrafocus">ultrafocus</a> TUI to block distracting websites and boost productivity.</p><div><hr></div><h3>Cut Code Review Time &amp; Bugs in Half (Sponsor)</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tbzq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:399652,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166184665?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!tbzq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 424w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 848w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1272w, https://substackcdn.com/image/fetch/$s_!tbzq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36390d4b-c304-4dfe-9435-f9c73ef7fa97_1456x728.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Code reviews are critical but time-consuming. <a href="https://coderabbit.link/alex">CodeRabbit</a> acts as your AI co-pilot, providing instant Code review comments and potential impacts of every pull request. </p><p>Beyond just flagging issues, CodeRabbit provides one-click fix suggestions and lets you define custom code quality rules using AST Grep patterns, catching subtle issues that traditional static analysis tools might miss.</p><p>CodeRabbit has so far reviewed more than 10 million PRs, installed on 1 million repositories, and used by 70 thousand Open-source projects. CodeRabbit is free for all open-source repo's!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://coderabbit.link/alex&quot;,&quot;text&quot;:&quot;Get Started Today&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://coderabbit.link/alex"><span>Get Started Today</span></a></p>]]></content:encoded></item><item><title><![CDATA[Optimizing multi-platform Docker builds (amd64 & arm64) with registry Cache]]></title><description><![CDATA[In modern CI/CD pipelines, supporting multiple architectures for Docker image builds, specifically amd64 and arm64, is crucial.]]></description><link>https://packagemain.tech/p/optimizing-multi-platform-docker</link><guid isPermaLink="false">https://packagemain.tech/p/optimizing-multi-platform-docker</guid><dc:creator><![CDATA[Julien Singler]]></dc:creator><pubDate>Tue, 17 Jun 2025 13:44:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!riLh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!riLh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!riLh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 424w, https://substackcdn.com/image/fetch/$s_!riLh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 848w, https://substackcdn.com/image/fetch/$s_!riLh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 1272w, https://substackcdn.com/image/fetch/$s_!riLh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!riLh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png" width="634" height="526" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebc078af-8db1-4e80-b00e-926927be5249_634x526.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:526,&quot;width&quot;:634,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13511,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/166141848?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!riLh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 424w, https://substackcdn.com/image/fetch/$s_!riLh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 848w, https://substackcdn.com/image/fetch/$s_!riLh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 1272w, https://substackcdn.com/image/fetch/$s_!riLh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febc078af-8db1-4e80-b00e-926927be5249_634x526.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>In modern CI/CD pipelines, supporting multiple architectures for Docker image builds, specifically <strong>amd64</strong> and <strong>arm64</strong>, is crucial. This ensures compatibility for your team, whether they're on Apple silicon or Intel machines, and aligns with your production infrastructure.</p><p>If you don&#8217;t have access to Github Runners or Machines in your CI/CD with both architectures, you might be using emulation, like QEMU and it can be quite slow to build an arm64 image on an amd64 machine. </p><p>That&#8217;s why you can optimize your builds and use cache to make it faster. </p><p>This article details a robust 3-step workflow for achieving efficient multiplatform builds and covers best practices for frontend (FE) projects where static assets should be built only once for amd64.</p><h2>Why multiplatform builds need special cache handling</h2><p>Docker's BuildKit and <a href="https://docs.docker.com/reference/cli/docker/buildx/">buildx</a> make multiplatform builds possible, but cache storage is architecture-specific. </p><p>If you build for both amd64 and arm64 in a single step and push cache to a registry, only one architecture's cache is stored due to cache overwriting. This leads to inefficient builds and missed cache hits on subsequent builds for the other architecture.</p><div><hr></div><h2>3-step workflow for optimized multiplatform caching</h2><p>To ensure both <code>amd64</code> and <code>arm64</code> builds benefit from proper cache, follow this three-step process:</p><h3>1. Build and cache for <code>arm64</code> (with <code>--load</code>)</h3><pre><code>docker buildx build \
 --cache-from=type=registry,ref=image:buildcache-arm64 \
 --cache-to=type=registry,ref=image:buildcache-arm64 \ 
 --platform=linux/arm64 \
 --load .</code></pre><p>or for those who use Github Actions: </p><pre><code>- name: Build and load cache for arm64
  uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/arm64
    load: true
    cache-from: type=registry,ref=image:buildcache-arm64
    cache-to: type=type=registry,ref=image:buildcache-arm64,compression=zstd,mode=max
    env:
      DOCKER_BUILDKIT: 1</code></pre><p>This command builds only for <code>arm64</code>, loads the result to the local Docker daemon, and updates the arm64-specific cache in the registry.</p><h3>2. Build and cache for <code>amd64</code> (with <code>--load</code>)</h3><pre><code>docker buildx build \
 --cache-from=type=registry,ref=image:buildcache-amd64 \
 --cache-to=type=registry,ref=image:buildcache-amd64 \ 
 --platform=linux/amd64 \
 --load .</code></pre><p>Similarly, this builds for <code>amd64</code> and updates the amd64-specific cache in the registry.</p><h3>3. Build final multiplatform image using both caches</h3><pre><code>docker buildx build \
 --cache-from=type=registry,ref=image:buildcache-arm64 \
 --cache-from=type=registry,ref=image:buildcache-amd64 \
 --platform=linux/amd64,linux/arm64 \
 -t myimage:latest \
 --push .</code></pre><p>or for those who use Github Actions:</p><pre><code>- name: Build and push image
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    platforms: linux/amd64, linux/arm64
    tags: myimage:latest
    cache-from: |
      type=registry,ref=image:buildcache-arm64
      type=registry,ref=image:buildcache-amd64
    env:
      DOCKER_BUILDKIT: 1</code></pre><p>This step imports both architecture caches, enabling maximum cache reuse, and pushes the multiplatform image to your registry.</p><p><strong>Note:</strong> Each cache must be exported to a unique registry reference to avoid overwriting.</p><div><hr></div><h2>Registry cache: best practices</h2><ul><li><p><strong>Separate cache references:</strong> Use distinct registry tags for each architecture (e.g., <code>image:buildcache-arm64</code>, <code>image:buildcache-amd64</code>).</p></li><li><p><strong>Import multiple caches:</strong> The final build should import both caches using multiple <code>--cache-from</code> flags.</p></li><li><p><strong>Builder consistency:</strong> Perform all steps on the same builder instance to ensure local cache continuity.</p></li></ul><div><hr></div><h2>Frontend projects: building static assets only once</h2><p>For frontend projects (e.g., React, Vue, Angular), static assets are architecture-agnostic. There's no need to build them for both <code>amd64</code> and <code>arm64</code>. Instead, let&#8217;s assume the machine in your CI is amd64:</p><ul><li><p><strong>Force Builder to </strong><code>amd64</code><strong>:</strong> Use <code>--platform=linux/amd64</code> or set <code>DOCKER_DEFAULT_PLATFORM=linux/amd64</code> to ensure static files are always built on <code>amd64</code>.</p></li><li><p><strong>Import Static Files in Final Multiplatform Layer:</strong> In your Dockerfile, copy the prebuilt static assets into the final image layer, which is then assembled as multiplatform.</p></li></ul><p><strong>Example:</strong></p><pre><code># Stage 1: Build static assets (amd64 only) 
FROM --platform=linux/amd64 node:18 AS fe-build 
WORKDIR /app 
COPY frontend/ . 
RUN npm ci &amp;&amp; npm run build 

# Stage 2: Multi-platform runtime 
FROM nginx:alpine
COPY --from=fe-build /app/build /usr/share/nginx/html</code></pre><ul><li><p>The <code>fe-build</code> stage always runs on <code>amd64</code>, while the final image can be built for both architectures.</p></li></ul><div><hr></div><h2>Key takeaways</h2><ul><li><p>Build and cache each architecture independently before building the final multiplatform image.</p></li><li><p>Always use unique cache references for each architecture to avoid cache overwriting.</p></li><li><p>For frontend projects, build static files only on <code>amd64</code> and import them into the multiplatform image.</p></li><li><p>Use Dockerfile's <code>--platform</code> flag or environment variables to control build architecture.</p></li></ul><p>By following these steps, you ensure fast, reliable, and cache-efficient multiplatform Docker builds for both backend and frontend projects.</p>]]></content:encoded></item><item><title><![CDATA[JSON Web Tokens in Go]]></title><description><![CDATA[JSON Web Tokens is a well known and popular open standard that defines a compact way for securely transmitting information between parties as a JSON object.]]></description><link>https://packagemain.tech/p/json-web-tokens-in-go</link><guid isPermaLink="false">https://packagemain.tech/p/json-web-tokens-in-go</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Sat, 24 May 2025 18:36:55 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b48f198f-b0bd-420e-b15a-9e6276acba69_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>JSON Web Tokens is a well known and popular open standard that defines a compact way for securely transmitting information between parties as a JSON object.</p><p>In this article we will dive into the standard itself to make sure we understand how JWT works. We will also implement a secure Go server that can issue JWTs and verify them. And finally we will review some best practices for securely using JWTs.</p><div class="pullquote"><p> The suggested pronunciation of JWT is the same as the English word "jot".</p></div><h2>Use Cases</h2><p>The most common use case for JWT is authorization. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Usually sent in a header, for example &#8220;Authorization&#8221;.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R-eS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R-eS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 424w, https://substackcdn.com/image/fetch/$s_!R-eS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 848w, https://substackcdn.com/image/fetch/$s_!R-eS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!R-eS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R-eS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png" width="1456" height="760" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:760,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:214286,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/164192611?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R-eS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 424w, https://substackcdn.com/image/fetch/$s_!R-eS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 848w, https://substackcdn.com/image/fetch/$s_!R-eS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!R-eS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cf9748d-766c-4851-ae57-8a0d8a19dfc4_2682x1400.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Note that on this diagram, the Authorization Server and the Resource Server are two separate entities, but technically they could be the same application.</p><p>Another use is information exchange. JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed, you can be sure the senders are who they say they are.</p><h2>Format</h2><pre><code>HEADER.PAYLOAD.SIGNATURE</code></pre><p>JWT consists of three concatenated Base64url-encoded strings, separated by dots:</p><ul><li><p><strong>Header</strong>: contains metadata about the type of token and the cryptographic algorithms used to secure its contents.</p></li><li><p><strong>JWS payload</strong> (set of <strong><a href="https://tools.ietf.org/html/rfc7519#section-4">claims</a></strong>): contains verifiable security statements, such as the identity of the user and the permissions they are allowed.</p></li><li><p><strong>JWS signature</strong>: used to validate that the token is trustworthy and has not been tampered with. When you use a JWT, you <strong>must</strong> <strong><a href="https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens">check its signature</a></strong> before storing and using it.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yTZK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yTZK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 424w, https://substackcdn.com/image/fetch/$s_!yTZK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 848w, https://substackcdn.com/image/fetch/$s_!yTZK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 1272w, https://substackcdn.com/image/fetch/$s_!yTZK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yTZK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png" width="1456" height="528" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:528,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:245855,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/164192611?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yTZK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 424w, https://substackcdn.com/image/fetch/$s_!yTZK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 848w, https://substackcdn.com/image/fetch/$s_!yTZK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 1272w, https://substackcdn.com/image/fetch/$s_!yTZK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54140d0b-c849-4216-b952-f49ba1e289af_2878x1044.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Payload contains:</p><ul><li><p><a href="https://www.rfc-editor.org/rfc/rfc7519#section-4.1">Registered claims</a>. The JWT specification defines seven reserved claims that are not required, but are recommended to allow interoperability with third-party applications. Some of them are: <strong>iss</strong> (issuer), <strong>exp</strong> (expiration time), <strong>sub</strong> (subject), <strong>aud</strong> (audience)</p></li><li><p>Custom claims, created to share information between parties that agree on using them and are neither registered or public claims.</p></li></ul><p>If you want to play with JWT and put these concepts into practice, you can use <a href="https://jwt.io">jwt.io Debugger</a> to decode, verify, and generate JWTs.</p><h2>Signature</h2><p>Signature is very important for security, so we can verify that no one tampered with our token data.</p><p>In general, JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA (although Auth0 supports only HMAC and RSA). When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.</p><p>Do not put secret information in the payload or header elements of a JWT unless it is encrypted. Anyone can read claims! Because it' just base64.</p><h2>Go Server</h2><p>In Go, the most popular package to work with JWT is <a href="https://golang-jwt.github.io/jwt/">golang-jwt/jwt</a>.</p><pre><code>go get -u github.com/golang-jwt/jwt/v5</code></pre><p>Below is a simple example in a single file of the HTTP server that does the following:</p><ul><li><p>Signing JWT tokens</p></li><li><p>Verify JWT tokens in middleware</p></li><li><p>Use RS256 as signing method</p></li><li><p>Uses <a href="https://echo.labstack.com/docs/cookbook/jwt">labstack/echo</a> router (my favorite one)</p></li></ul><h2>Step 1. PEM keys</h2><p>There are different signing methods. The most popular ones are HS256 and RS256.</p><p>HS256 is symmetric, which requires the decoder to also have the secret private key, which may be an issue.</p><p>RSA on the other hand is assymetric, which means that there are two keys: one public key and one private key that must be kept secret.</p><p>The most secure practice, is to use RS256 because:</p><ul><li><p>With RS256, you are sure that only the holder of the private key can sign tokens, while anyone can check if the token is valid using the public key.</p></li><li><p>With RS256, if the private key is compromised, you can implement key rotation without having to re-deploy your application or API with the new secret (which you would have to do if using HS256).</p></li></ul><p>Let&#8217;s generate our private and public RSA keys:</p><pre><code><code>openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem</code></code></pre><p>We can then load them in our Go Server:</p><pre><code>var (
&#9;publicKey  *rsa.PublicKey
&#9;privateKey *rsa.PrivateKey
)

// Load both public (verify) and private (sign) RSA keys
func init() {
&#9;publicKeyData, err := os.ReadFile("./public_key.pem")
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}
&#9;publicKey, err = jwt.ParseRSAPublicKeyFromPEM(publicKeyData)
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}

&#9;privateKeyData, err := os.ReadFile("./private_key.pem")
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}
&#9;privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(privateKeyData)
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}
}</code></pre><p>Make sure you store these PEM keys securely, for example using Kubernetes Secrets.</p><h2>Step 2. Issuer</h2><p>Our server will have an open login endpoint to exchange credentials for a JWT. Here we can go to the database and verify the password (we&#8217;ll skip this part in the example).</p><p>The party that issues the JWT needs to use the private RSA key.</p><pre><code>func main() {
&#9;e := echo.New()
&#9;e.Use(middleware.Logger())

&#9;e.POST("/login", login)

&#9;e.Start("127.0.0.1:4242")
}

// We can add custom claims here
type jwtClaims struct {
&#9;jwt.RegisteredClaims
}

func login(c echo.Context) error {
&#9;username := c.FormValue("username")
&#9;password := c.FormValue("password")

&#9;// TODO: implement real auth by checking user in the database
&#9;if username != "package" || password != "main" {
&#9;&#9;return echo.ErrUnauthorized
&#9;}

&#9;// Set expiration time (1h)
&#9;claims := &amp;jwtClaims{
&#9;&#9;jwt.RegisteredClaims{
&#9;&#9;&#9;Subject:   username,
&#9;&#9;&#9;ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
&#9;&#9;},
&#9;}

&#9;token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

&#9;t, err := token.SignedString(privateKey)
&#9;if err != nil {
&#9;&#9;return echo.ErrInternalServerError
&#9;}

&#9;return c.JSON(http.StatusOK, echo.Map{
&#9;&#9;"token": t,
&#9;})
}</code></pre><p>The result of a successful authentication will be a JWT token that expires in 1 hour. Once the client receives the token, there is no way to invalidate it (expire). To minimize misuse of a JWT, the expiry time is usually kept in the order of a few minutes. Typically the client application would refresh the token in the background using the <strong>/refresh</strong> route for exanple.</p><h2>Step 3. Middleware</h2><p>Once the user is logged in, each subsequent request will include the JWT, allowing the user to access the &#8220;/api&#8220; route. Usually done via the &#8220;Authorization&#8220; header.</p><p>The verify uses the public PEM key.</p><pre><code>func main() {
&#9;e := echo.New()
&#9;e.Use(middleware.Logger())

&#9;config := echojwt.Config{
&#9;&#9;NewClaimsFunc: func(c echo.Context) jwt.Claims {
&#9;&#9;&#9;return new(jwtClaims)
&#9;&#9;},
&#9;&#9;SigningKey:    publicKey,
&#9;&#9;SigningMethod: jwt.SigningMethodRS256.Name,
&#9;}

&#9;g := e.Group("/api")
&#9;g.Use(echojwt.WithConfig(config))
&#9;g.GET("/greet", greet)

&#9;e.Start("127.0.0.1:4242")
}

type jwtClaims struct {
&#9;jwt.RegisteredClaims
}

func greet(c echo.Context) error {
&#9;user := c.Get("user").(*jwt.Token)
&#9;claims := user.Claims.(*jwtClaims)
&#9;sub := claims.Subject
&#9;return c.String(http.StatusOK, fmt.Sprintf("hi %s!", sub))
}</code></pre><h2>The JOSE framework</h2><p>It&#8217;s also important to mention that apart from JWT, there are other very related standards that are usually called a JOSE framework and include:</p><ul><li><p>JWS: JSON Web Signature, a specification for digitally signing JSON data.</p></li><li><p>JWE: JSON Web Encryption, a specification for encrypting JSON data.</p></li><li><p>JWK: JSON Web Key, a specification for representing cryptographic keys in JSON format.</p></li><li><p>JWT: JSON Web Token, a compact and standardized way of securely transmitting information between parties using JSON data structures.</p></li><li><p>JWA: JSON Web Algorithms, a specification for defining cryptographic algorithms used in JWE and JWS.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6_6t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6_6t!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 424w, https://substackcdn.com/image/fetch/$s_!6_6t!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 848w, https://substackcdn.com/image/fetch/$s_!6_6t!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 1272w, https://substackcdn.com/image/fetch/$s_!6_6t!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6_6t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png" width="351" height="321.6015228426396" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:722,&quot;width&quot;:788,&quot;resizeWidth&quot;:351,&quot;bytes&quot;:297455,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/164192611?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6_6t!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 424w, https://substackcdn.com/image/fetch/$s_!6_6t!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 848w, https://substackcdn.com/image/fetch/$s_!6_6t!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 1272w, https://substackcdn.com/image/fetch/$s_!6_6t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F537648f7-d98b-4f41-8d70-cc05d8bffbb2_788x722.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>And you can use this useful package <a href="https://github.com/lestrrat-go/jwx">lestrrat-go/jwx</a> that has implementation of these JOSE technologies for Go.</p><h2>Security Best Practices</h2><ul><li><p>Keep it safe and simple, do not put secret info into JWT. It is because JWTs are not encrypted (only encoded with base64) and can be read by anyone if leaked.</p></li><li><p>Embrace HTTPS. TTPS (SSL/TLS) prevents interception and Man-in-the-Middle attacks, ensuring tokens are transmitted securely.</p></li><li><p>Use RS256 (asymmetric signing) with public/private key pairs. HS256 is more performant though.</p></li><li><p>Carefully store JWT. For example local storage is not the best place, due to security risks like XSS (Cross-Site Scripting) attacks.</p></li></ul><h2>Resources</h2><ul><li><p><a href="https://github.com/plutov/packagemain/tree/master/jwtdemo">Source Code</a></p></li></ul><h2>In case you prefer a video format</h2><div id="youtube2-aIaXSpLkvAg" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;aIaXSpLkvAg&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/aIaXSpLkvAg?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div>]]></content:encoded></item><item><title><![CDATA[Real-Time database change tracking in Go: Implementing PostgreSQL CDC]]></title><description><![CDATA[Introduction]]></description><link>https://packagemain.tech/p/real-time-database-change-tracking</link><guid isPermaLink="false">https://packagemain.tech/p/real-time-database-change-tracking</guid><dc:creator><![CDATA[Julien Singler]]></dc:creator><pubDate>Wed, 07 May 2025 14:36:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!30RQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!30RQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!30RQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 424w, https://substackcdn.com/image/fetch/$s_!30RQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 848w, https://substackcdn.com/image/fetch/$s_!30RQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 1272w, https://substackcdn.com/image/fetch/$s_!30RQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!30RQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png" width="728" height="310.75620975160996" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:464,&quot;width&quot;:1087,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:33758,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/161408784?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!30RQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 424w, https://substackcdn.com/image/fetch/$s_!30RQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 848w, https://substackcdn.com/image/fetch/$s_!30RQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 1272w, https://substackcdn.com/image/fetch/$s_!30RQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41af0b9f-64b5-4d4f-abb4-f9345cbb32a4_1087x464.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Introduction</strong></h2><p>Change Data Capture (CDC) enables real-time tracking of database changes, critical for event-driven systems, analytics pipelines, and synchronizing microservices. This guide walks through implementing PostgreSQL CDC in Go using native logical replication and the <code>pgx</code> driver.</p><h2><strong>1. Understanding PostgreSQL CDC</strong></h2><p>PostgreSQL provides CDC via <strong>logical replication</strong>, which decodes changes from the write-ahead log (WAL) into consumable events (inserts/updates/deletes). Key concepts:</p><ul><li><p><strong>Replication Slots</strong>: Persistent channels for streaming changes.</p></li><li><p><strong>Publications</strong>: Define which tables to monitor.</p></li><li><p><strong>Logical Decoding Plugins</strong>: Convert WAL entries to readable formats (e.g., <code>pgoutput</code>, <code>wal2json</code>).</p></li></ul><h2><strong>2. Prerequisites</strong></h2><p><strong>a) PostgreSQL configured for replication</strong></p><pre><code># postgresql.conf  
wal_level = logical  
max_replication_slots = 5</code></pre><p>In GCP with cloudSQL, you have to enable some flags:</p><pre><code>cloudsql.logical_decoding = on </code></pre><p><strong>b) Replication user</strong></p><pre><code>CREATE ROLE repl_user WITH LOGIN REPLICATION PASSWORD 'password';  
GRANT SELECT ON ALL TABLES IN SCHEMA public TO repl_user;</code></pre><h2><strong>3. Implementing CDC in Go</strong></h2><h3>Preambule:</h3><p><a href="https://github.com/jackc/pglogrepl">pglogrepl</a> is a Go library designed for interacting with PostgreSQL&#8217;s logical replication protocol. Built atop the <a href="https://github.com/jackc/pgconn">github.com/jackc/pgx/v5/pgconn</a> package, it enables Go applications to connect to PostgreSQL databases and process logical replication messages, making it well-suited for building Change Data Capture (CDC) solutions, streaming data pipelines or custom replication clients.. </p><p>Note: replication=database in the DSN is actually required</p><pre><code>
import (
   "github.com/jackc/pgx/v5"
   "github.com/jackc/pgx/v5/pgconn"
)

type event struct {
&#9;Relation string
&#9;Columns  map[string]any
&#9;Operation     string
}

func main() {
  conn, _ := pgx.Connect(context.Background(), "postgres://repl_user:password@localhost:5432/dbname?replication=database")

   c := make(chan event)
   ReadAndDefer(context.Background(), conn.PgConn(), c)
}</code></pre><h3>Step 1: Start replication </h3><pre><code>func startReplication(ctx context.Context, conn *pgconn.PgConn) error {
&#9;var err error
&#9;
        pubName := os.Getenv("PUBLICATION_NAME")
&#9;// 1. Drop publication if it exists
&#9;if _, err := conn.Exec(ctx, fmt.Sprintf("DROP PUBLICATION IF EXISTS %s;", pubName)).ReadAll(); err != nil {
&#9;&#9;return err
&#9;}
        // create the publication for all tables
&#9;if _, err := conn.Exec(ctx, fmt.Sprintf("CREATE PUBLICATION %s FOR ALL TABLES;", pubName)).ReadAll(); err != nil {
&#9;&#9;return err
&#9;}

&#9;// 3. create temporary replication slot server
&#9;if _, err = pglogrepl.CreateReplicationSlot(ctx, conn, os.Getenv("SLOT_NAME"), os.Getenv("OUTPUT_PLUGIN"), pglogrepl.CreateReplicationSlotOptions{Temporary: true}); err != nil {
&#9;&#9;return err
&#9;}

&#9;pluginArguments := []string{
&#9;&#9;"proto_version '1'",
&#9;&#9;fmt.Sprintf("publication_names '%s'", pubName),
&#9;&#9;"messages 'true'",
&#9;}

&#9;// 4. establish connection
&#9;return pglogrepl.StartReplication(ctx, conn, os.Getenv("SLOT_NAME"), pglogrepl.ParseLSN("0/0"), pglogrepl.StartReplicationOptions{PluginArgs: pluginArguments})
}</code></pre><h3>Step 2: receive message</h3><pre><code>func ReadAndDefer(ctx context.Context, conn *pgconn.PgConn, c chan event) error {
    err := startReplication(ctx, conn)
    ...
    relations := map[uint32]*pglogrepl.RelationMessage{}
    typeMap := pgtype.NewMap()

    for {
        msg, err := conn.ReceiveMessage(ctx)

        switch msg := msg.(type) {
&#9;case *pgproto3.CopyData:
&#9;   switch msg.Data[0] {
&#9;   case pglogrepl.PrimaryKeepaliveMessageByteID:
              // Parse primary keep alive message
              // update client XLogPos if necessary
&#9;   case pglogrepl.XLogDataByteID:
&#9;      walLog, err := pglogrepl.ParseXLogData(msg.Data[1:])
&#9;      if err != nil {
&#9;         log.Printf("failed to parse logical WAL log: %v", err)
&#9;      }
&#9;      if err := parseAndDispatchMessage(&amp;walLog, relations, typeMap, c); err != nil {
&#9;          log.Printf("failed to parse and dispatch message: %v", err)
&#9;      }
&#9;}
&#9;default:
&#9;    log.Printf("received unexpected message: %T\n", msg)
&#9;}
    }
}</code></pre><h3>Step 3: Parse messages</h3><pre><code>func decodeTextColumnData(mi *pgtype.Map, data []byte, dataType uint32) (any, error) {
   if dt, ok := mi.TypeForOID(dataType); ok {
      return dt.Codec.DecodeValue(mi, dataType, pgtype.TextFormatCode, data)
   }
   return string(data), nil
}


func parseAndDispatchMessage(walLog *pglogrepl.XLogData, relations map[uint32]*pglogrepl.RelationMessage, typeMap *pgtype.Map, c chan event) error {
&#9;msg, err := pglogrepl.Parse(walLog.WALData)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;switch m := msg.(type) {
&#9;case *pglogrepl.RelationMessage:
&#9;    relations[m.RelationID] = m
&#9;case *pglogrepl.InsertMessage:
&#9;    rel, ok := relations[m.RelationID]
&#9;    if !ok {
&#9;        log.Printf("relation %d not found", m.RelationID)
&#9;&#9;break
&#9;    }
&#9;    values := map[string]any{}
&#9;    for idx, col := range m.Tuple.Columns {
&#9;&#9;colName := rel.Columns[idx].Name
&#9;&#9;switch col.DataType {
&#9;&#9;case 'n': // null
&#9;&#9;    values[colName] = nil
&#9;&#9;case 't': // text
&#9;&#9;    val, _ := decodeTextColumnData(typeMap, col.Data, rel.Columns[idx].DataType)
&#9;&#9;    values[colName] = val
&#9;&#9;}
&#9;&#9;event := event{
&#9;&#9;   Relation: rel.RelationName,
&#9;&#9;   Columns:  values,
&#9;&#9;   Operation:     "insert",
&#9;&#9;}
&#9;&#9;c &lt;- event
&#9;case *pglogrepl.UpdateMessage:
            ...
&#9;case *pglogrepl.DeleteMessage:
            ...
&#9;}
&#9;return nil
}</code></pre><h2><strong>5. Handling Decoded Changes</strong></h2><p>The <code>pglogrepl</code> library parses raw WAL data into structured messages. For example, an <code>InsertMessage</code> contains:</p><ul><li><p><strong>RelationID</strong>: Table identifier</p></li><li><p><strong>Tuple</strong>: New row values</p></li><li><p><strong>Columns</strong>: Metadata about columns</p></li></ul><p>Example Output:</p><pre><code>INSERT: {RelationID: 16393, Tuple: [{Column: user_id, Value: 123}, ...]}</code></pre><h2>Drawbacks</h2><ul><li><p>No automatic schema replication: (ALTER TABLE ..) must be manually applied to subscribers.</p></li><li><p>Primary Key requirement: tables must have a primary key or unique index for replication. </p></li><li><p>Large objects: BLOB/CLOB are not replicated</p></li><li><p>Non-table objects: Views, materialized views and foreign tables are excluded.</p></li><li><p>The complexity around handling replication versus the simplicity of other solutions. (tradeoff performance vs maintenance)</p></li></ul><h2><strong>Conclusion</strong></h2><p>PostgreSQL's logical replication paired with Go's <code>pgx</code> provides a robust foundation for building CDC pipelines. By streaming changes directly from the WAL, you achieve low-latency access to database events while minimizing performance overhead.</p><p><strong>Resources:</strong></p><ul><li><p><a href="https://pkg.go.dev/github.com/jackc/pgx/v5">pgx Documentation</a></p></li><li><p><a href="https://www.postgresql.org/docs/current/logical-replication.html">PostgreSQL Logical Replication Guide</a></p></li><li><p><a href="https://github.com/jackc/pglogrepl/blob/master/example/pglogrepl_demo/main.go">pglogrepl Example Code</a></p></li></ul><p>This implementation gives you full control over change processing, making it ideal for building reactive systems that respond instantly to database changes.</p>]]></content:encoded></item><item><title><![CDATA[Comparing error handling in Zig and Go]]></title><description><![CDATA[Errors are values]]></description><link>https://packagemain.tech/p/comparing-error-handling-in-zig-and</link><guid isPermaLink="false">https://packagemain.tech/p/comparing-error-handling-in-zig-and</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Mon, 28 Apr 2025 14:15:03 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8fa65d7a-6b38-4148-8f85-0b8b7317dd95_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div id="youtube2-E8LgbxC8vHs" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;E8LgbxC8vHs&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/E8LgbxC8vHs?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>Errors are values</h2><p>I've been using Go for probably 8 years, and it's been my go-to language for most of the projects. I think we can all agree that Go can be verbose sometimes, which attracts some people (like me) but also is a hot topic often. Specifically when it comes to error handling, when the big portion of your codebase might be pure error handling.</p><p>What's beautiful in Go about error handling is that <a href="https://go.dev/blog/errors-are-values">errors are values</a>, this simplifies passing them around, modifying them as you want, because as Rob Pike said:</p><div class="pullquote"><p>Values can be programmed, and since errors are values, errors can be programmed.</p><p><br>- Rob Pike -</p></div><p>To summarize, a function in Go can return an error, or even multiple:</p><pre><code><code>func Open(name string) (*File, error)</code></code></pre><p>The users of this function can simply check for <code>nil</code> error value:</p><pre><code><code>f, err :=  os.Open("main.zig")
if err !=  nil {
    // process the error
}

// one-liners are also possible
if err := f.Close(); err != nil {
    log.Fatal(err)
}</code></code></pre><p><code>error</code> type is an interface type, which makes it possible to create custom error types that can hold various data.</p><pre><code><code>type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

func New(text string) error {
    return &amp;errorString{text}
}</code></code></pre><p>That's basically it, there is also an `errors` package that has some handy utilities for working with errors in Go. For example you can check the error type by doing:</p><pre><code>if errors.Is(err, fs.ErrNotExist) {
    // process error
}</code></pre><p>You can read about error handling in Go in detail <a href="https://go.dev/blog/error-handling-and-go">here</a>, but as you probably noticed from the title of this article, we wanted to talk about Zig here as well, specifically about its error handling as well.</p><h2>Few notes on Zig</h2><p>Heads up, I'm still new to Zig, yes I wrote few dead simple <a href="https://github.com/plutov/zigping">programs</a>, but I'm still learning.</p><p>I found some similarities between Go and Zig, as both languages have adopted a design philosophy of simplicity to remove the language out of the way to let you be productive quickly.</p><p>Zig, however, stays true to its "no hidden control flow" and requires a programmer to manage the memory allocations manually. I always programmed in languages with GC, so I was used to the fact that allocations happen behind the scenes.</p><p>Memory allocation can fail too, and Zig makes allocation failures explicit, so here is our segue into error handling in Zig!</p><h2>Error handling in Zig: 101</h2><p>Zig like Go treats errors like values, but does it through a specialized <code>enum</code> that can be created implicitly.</p><p>Now, this is where things get interesting. The function below returns an <code>!usize</code> where the exclamation mark <code>!</code> indicates that this function can return an error. We can either be explicit about the error(s) that our function returns or implicit. In this case, we've taken the implicit route.</p><pre><code><code>fn get_args_count(allocator: std.mem.Allocator) !usize {
    const args = try std.process.argsAlloc(allocator);
    if (args.len &lt; 1) {
        return error.EmptyArgs;
    }

    return args.len;
}</code></code></pre><p>This code listing shows another mechanism of propagating an error up the call stack using the <code>try</code> keyword. We must do that, because <code>std.process.argsAlloc</code> might return an error, and all errors in Zig must be handled or we'll get a compile time error. The equivalent Go code for &#8220;<code>const args = try std.process.argsAlloc(allocator);</code>&#8220; would be:</p><pre><code><code>args, err := std.process.argsAlloc(allocator)
if err != nil {
    return nil, err
}</code></code></pre><p>Zig version is definitely more concise. But what if you want to handle the error first? Similar to exception handling in other languages, Zig uses the <code>catch</code> keyword to intercept errors instead of propagating the error up the call stack.</p><pre><code><code>// notice here, our main function can't return an error,
// otherwise its return type would be !void
pub fn main() void {
    const args_count = get_args_count(allocator) catch |err| {
        // do some stuff, maybe log an error
        std.debug.print("invalid input: {}\n", .{err});
        return;
    };
}</code></code></pre><p>By the way, this can be nicely paired with a fallback return using the following 2 forms. Simple fallback value:</p><pre><code><code>const args_count = get_args_count(allocator) catch 0;</code></code></pre><p>Or by using Zig's <a href="https://ziglang.org/documentation/master/#Blocks">named blocks</a>:</p><pre><code><code>const args_count = get_args_count(allocator) catch blk: {
    // do some stuff, maybe log an error

    // and then return a result
    break :blk 0;
};</code></code></pre><p>And lastly, we can use <code>if-else-switch</code> construct to handle potential errors more precisely:</p><pre><code><code>if (get_args_count(allocator)) |args_count| {
    std.debug.print("got {d} args\n", .{args_count});
} else |err| switch (err) {
    error.EmptyArgs =&gt; {
        std.debug.print("invalid input: no args\n", .{});
    },
    else =&gt; {
        std.debug.print("unexpected error: {}\n", .{err});
    },
}
</code></code></pre><p>Remember I mentioned that in Zig selectively omitting error handling is not allowed? That's true, but sometimes you want skip error handling in places where it's not really necessary, for example tests or build code, or some quick scripts. For that you can use <code>catch unreachable</code> in functions that do not return an error, but that could possibly panic.</p><pre><code><code>pub fn main() void {
    const args_count = get_args_count(allocator) catch unreachable;
    std.debug.print("got {d} args\n", .{args_count});
}</code></code></pre><h2>Zig errors and context</h2><p>At the end of the day, Zig errors are just enum values and don't contain much information or behaviour, however they do carry (not in the enum itself) a <a href="https://ziglang.org/documentation/master/#Error-Return-Traces">debugging trace</a> which is in theory is accessible at runtime with <a href="https://ziglang.org/documentation/master/#errorReturnTrace">errorReturnTrace</a>.</p><p>Contrary to Zig (as we've seen in Go section), in Go, errors are interfaces, allowing them to be concrete types that can carry additional context.</p><p>For instance, when dealing with file operations, a Go function might return an error of type <a href="https://pkg.go.dev/os#PathError">os.PathError</a>. This type includes fields like <code>Op</code> (the operation), <code>Path</code> (the file path) and the underlying system error. And this is very helpful. There was an <a href="https://github.com/ziglang/zig/issues/2647">open issue</a> in Zig to allow for something like that, but it didn't happen and developers can use tagged unions instead to create generic results.</p><h2>Conclusion</h2><p>There are different approaches to handle errors and every language does it differently. I personally find that both Go and Zig have the best error handling compared to other languages, that's the reason why I wanted to write this article and give you a tiny taste of Zig's error handling.</p><p>Both Go and Zig treat errors as values, emphasizing explicit handling. Zig's approach, with its specialized enum, try, and catch keywords, presents a feature-rich and concise way to manage errors.</p><p>While Go's error-as-value using interfaces is straightforward and allows for rich contextual errors (like <code>os.PathError</code>), it can often lead to more verbose code due to explicit <code>if err != nil</code> checks. Despite this verbosity, Go's simplicity makes it easy to grasp.</p><p>Let me know in the comments what you think about error handling in these 2 programming languages.</p><h2>Resources</h2><ul><li><p><a href="https://go.dev/blog/errors-are-values">Errors are values</a></p></li><li><p><a href="https://ziglang.org/documentation/master/#Errors">Errors in Zig #1</a></p></li><li><p><a href="https://zig.guide/language-basics/errors/">Errors in Zig #2</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[How to use the new "tool" directive]]></title><description><![CDATA[Go 1.24 new "tool" directive.]]></description><link>https://packagemain.tech/p/how-to-use-the-new-tool-directive</link><guid isPermaLink="false">https://packagemain.tech/p/how-to-use-the-new-tool-directive</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Sat, 12 Apr 2025 20:07:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d14873f6-57fe-4d41-9928-569a66efdda0_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Go build system is known for its efficiency and continuous improvement. Go 1.24 introduces a significant enhancement with the &#8220;tool&#8221; directive, addressing how Go-based dev tools are managed.</p><p>Previously, managing tools like linters (<strong>golangci-lint</strong>), code generators (<strong>mockery, oapi-codegen</strong>), or other build utilities often involved manual installation steps (using &#8220;go install&#8221; or some package manager) outside the project's dependency management, leading to potential version inconsistencies across development environments and CI systems.</p><p>I&#8217;ve seen it many times when two developers generate different code by using the same command, only because the versions of these tools are different. There are workarounds to &#8220;fix&#8220; the versions in fake &#8220;tools.go&#8221; file, but it&#8217;s not enough and felt dirty.</p><p>But not anymore! <a href="https://tip.golang.org/doc/go1.24#tools">Go 1.24 addresses</a> these challenges directly with the &#8220;tool&#8221; directive.</p><h2><strong>Adding a new tool to go.mod</strong></h2><p>Let&#8217;s take oapi-codegen as an example, to add it to our Go project we can simply use &#8220;go get -tool&#8220;:</p><pre><code>go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest</code></pre><p>Once installed, the tool is available for use via &#8220;go tool&#8220;. Note, that the first run could be a bit slow because Go will compile the tool if it&#8217;s not yet compiled, repeated executions are much faster. Which should not be a big problem for dev tools, but worth mentioning.</p><pre><code>go tool oapi-codegen</code></pre><p>Note, that oapi-codegen could be installed globally with a different version. Calling &#8220;go tool&#8220; without a tool name will list all the tools. Note there are some built-in to Go, so you will always have a few.</p><p>The tool will be added to a project and listed in our <strong>go.mod</strong> file, so all users of our software will understand what tools and what versions are required.</p><pre><code>tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen

require (
  github.com/oapi-codegen/runtime v1.1.1
)</code></pre><p>By the way, while not a huge deal, I find &#8220;go tool toolname&#8220; a bit verbose as you always have to type it. But it doesn&#8217;t matter if you use &#8220;go:generate&#8220; for example.</p><h2><strong>Pairing with go:generate</strong></h2><p>What's also great is that we can leverage that in <strong>go:generate</strong> annotations, which completely eliminates a manual tool installations. Traditionally, we would rely on the tool being installed globally on the host:</p><p><em>main.go:</em></p><pre><code>// go:generate oapi-codegen openapi.yaml</code></pre><p>But now we could use the &#8220;tool&#8220;:</p><pre><code>// go:generate go tool oapi-codegen openapi.yaml</code></pre><p>Which will use the correct version pinned in your go.mod file.</p><pre><code># A single command to run all generators!

go generate</code></pre><h2><strong>Separate go.mod file</strong></h2><p>It is also possible to use a separate go.mod file for dev tools in case you don&#8217;t want to mix it with your regular dependencies or you have some issues with the versions clashing.</p><pre><code>go tool -modfile=tools.mod oapi-codegen</code></pre><h2><strong>Non-Go tools</strong></h2><p>Only Go tools are supported. However, some projects will likely depend on some non-Go tools, which have to be managed separately.</p><h2><strong>Conclusion</strong></h2><p>With the introduction of the &#8220;tool&#8221; directive in Go 1.24, Go modules gain a much-improved way to handle tool dependencies, similar to &#8220;npm run&#8220; in Node.js world.</p><h2><strong>Resources</strong></h2><ul><li><p><a href="https://tip.golang.org/doc/go1.24#tools">Official docs</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Practical OpenAPI in Go]]></title><description><![CDATA[From OpenAPI Specification to Go Server.]]></description><link>https://packagemain.tech/p/practical-openapi-in-golang</link><guid isPermaLink="false">https://packagemain.tech/p/practical-openapi-in-golang</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Wed, 12 Feb 2025 14:59:50 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f3c756a8-04f7-486c-901d-77f2f1dfe713_3200x1800.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div id="youtube2-87au30fl5e4" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;87au30fl5e4&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/87au30fl5e4?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>Well-structured and documented APIs are pleasure to work with. And nowadays the de-facto standard is <a href="https://www.openapis.org/">OpenAPI</a>, which comes with a good methodology of defining an API interface first, and only then constructing everything around it. Making it easier to understand, implement, and consume those APIs. And standards matter, they allow different teams, regardless of their technology stack, to effectively communicate about and work with the same API.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aB5O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aB5O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 424w, https://substackcdn.com/image/fetch/$s_!aB5O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 848w, https://substackcdn.com/image/fetch/$s_!aB5O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 1272w, https://substackcdn.com/image/fetch/$s_!aB5O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aB5O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png" width="1295" height="1490" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1490,&quot;width&quot;:1295,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:160230,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aB5O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 424w, https://substackcdn.com/image/fetch/$s_!aB5O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 848w, https://substackcdn.com/image/fetch/$s_!aB5O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 1272w, https://substackcdn.com/image/fetch/$s_!aB5O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3191c4bd-690b-45e8-86bb-8b460ae434c6_1295x1490.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">API Lifecycle</figcaption></figure></div><p>In this practical guide I want to walk you through all important parts involved in architecting, implementing and consuming the API with help of OpenAPI standard.</p><p>Before we dive in, it's helpful to have a basic understanding of the following:</p><ul><li><p>Go programming language</p></li><li><p>RESTful APIs</p></li><li><p>JSON/YAML</p></li><li><p>Basic command-line usage</p></li></ul><h2>What is OpenAPI and OAS?</h2><p>The <a href="https://spec.openapis.org/">OpenAPI Specification (OAS)</a> was originally based on the Swagger 2.0 Specification from SmartBear Software. Later it was moved to the <a href="https://www.openapis.org/">OpenAPI Initiative (OAI)</a>, a consortium of industry experts under the Linux Foundation.</p><p>The main idea of OpenAPI is to be able to describe in agnostic terms, decoupling them from any specific programming language. Consumers of your API specification do not need to understand the guts of your application or try to learn Lisp or Haskell if that&#8217;s what you chose to write it in. They can understand exactly what they need from your API specification, written in a simple and expressive language.</p><p>This simple and expressive language is called <a href="https://www.jetbrains.com/mps/concepts/domain-specific-languages/">DSL (domain specific language)</a>. It can be written in either JSON or YAML.</p><p>The latest version of OAS is <a href="https://spec.openapis.org/oas/latest.html">v3.1.1</a> and the specification itself is huge, there are many features and corner cases, but we will try to go through the most important ones.</p><h2>Architecting the API</h2><p>It all starts with defining what the API should provide for its consumers and what it is for. While this stage isn't always purely technical, having a sketch of your API design in OAS when gathering requirements gives you a headstart when starting design.</p><p>Once the requirements are ready, it's time to open your <a href="https://editor.swagger.io/">OpenAPI editor</a> and collaborate with your teammates.</p><p>And it's important to understand that it's not only about writing JSON/YAML spec, but actually agreeing on the API design.</p><p>It's recommended to follow some API design guide, Google has <a href="https://cloud.google.com/apis/design">one</a> for example. So you don't end up having mixed styles like <strong>/resourceName/{id}</strong> and <strong>/resource_name/{id}</strong>, inconsistent use of HTTP methods, or unclear resource relationships.</p><h2>openapi.yaml</h2><p>The spec of your API starts in the entrypoint document <code>openapi.yaml</code> (recommended but not required name) or <code>openapi.json</code>. I've seen very big <code>openapi.yaml</code> files (50k lines), but it's possible to split your spec into multiple parts. However, this may not work well for some OpenAPI tools as they expect a single file. <a href="https://github.com/googlemaps/openapi-specification/">Google Maps OAS</a> is a good example on how to split the schema, but also comes with a pre-processor to generate a single file.</p><p>There are some open source tools to bundle the OAS: <a href="https://github.com/APIDevTools/swagger-cli">swagger-cli</a> (archived) and <a href="https://github.com/Redocly/redocly-cli">redocly-cli</a> are great options.</p><pre><code><code>swagger-cli bundle -o _bundle/openapi.yaml openapi.yaml</code></code></pre><p>As I mentioned earlier, the spec is huge, but let's break it into smaller parts. For this post I created a dummy "Smart Home" API, you can see the full spec and code <a href="https://github.com/plutov/packagemain/tree/master/oapi-example">here</a>.</p><p>The root object is called <a href="https://spec.openapis.org/oas/latest.html#openapi-object">OpenAPI Object</a> and has the following structure:</p><pre><code><code># schema version
openapi: 3.1.1

# docs
info:
  title: Smart Home API
  description: API Specification for Smart Home API
  version: 0.0.1

# optional servers for public APIs
servers:
  - url: "https://..."

# tags are used to group the endpoints
tags:
  - name: device
    description: Manage devices
  - name: room
    description: Manage rooms

# endpoints go here
paths:
  # ...

# reusable objects such as schemas, error types, request bodies
components:
  # ...

# security mechanisms, should correspond to components.securitySchemes
security:
  - apiKeyAuth: []</code></code></pre><p>We defined the skeleton of our schema, but the majority of OpenAPI schema lays in <code>paths</code> and <code>components</code> props.</p><h2>Paths and Operations</h2><p>Let's now add few endpoints to our schema. The operations are grouped by paths, so you can have multiple HTTP methods on a single path, for example <code>GET /devices/{deviceId}</code> and <code>DELETE /devices/{deviceId}</code>.</p><p>It's a good practice to define all types (request bodies, responses, errors) in <code>components</code> section and reference them instead of manually defining them in <code>paths</code> section. This allows for easier re-use of entities. For example in our API we have a type <code>Device</code> which can be used in many endpoints.</p><pre><code><code>paths:

  # the path has a parameter in it
  /devices/{deviceId}:
    get:
      tags:
        - device
      summary: Get Device
      operationId: getDevice

      parameters:
        - name: deviceId
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/ULID"

      responses:

        "200":
          description: Success
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Device"

        "404":
          description: Not Found
          content:
            application/json:
              schema:
                # use common type for 404 errors
                $ref: "#/components/schemas/ErrorNotFound"</code></code></pre><p>In the spec above we defined two endpoints of our API and referenced the types which we still need to define: <code>Device</code>, <code>ErrorNotFound</code> and <code>ULID</code>. Notice that for <code>deviceId</code> path param we also used a custom type instead of a standard string, which can be helpful in the future in case we want to change the format of our IDs (for example UUID, ULID. integer, etc.).</p><p>Notice that each operation has a unique <code>operationId</code>, while it's optional it's very helpful to set one, so then it can be used on the server and client sides.</p><p>This is a basic configuration and can be extended further if we want to. For example, when serving this schema in Swagger, it's good to see the examples of our requests (and their variations). We can define it here in <code>responses</code> section, or directly in our <code>components.schemas</code>.</p><pre><code><code>responses:
  "200":
    content:
      application/json:
        examples:
          new_device:
            value: # any value
</code></code></pre><h2>Schemas</h2><p><code>components</code> is an integral part of OAS, it contains the following properties:</p><ul><li><p>schemas</p></li><li><p>responses</p></li><li><p>parameters</p></li><li><p>requestBodies</p></li><li><p>headers</p></li><li><p>securitySchemes</p></li><li><p><a href="https://spec.openapis.org/oas/latest.html#components-object">see all here</a></p></li></ul><p>We could define our <code>Device</code> type like this:</p><pre><code><code>components:
  schemas:
    Device:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/ULID'
        name:
          type: string
      required:
        - id
        - name</code></code></pre><p>But later you may have other types that have <code>name</code> or <code>id</code> fields, so it's recommended to define them separately and combined in final type using <code>allOf</code>:</p><pre><code><code>components:
  schemas:
    WithId:
      type: object
      required:
        - id
      properties:
        id:
          $ref: "#/components/schemas/ULID"

    WithName:
      type: object
      required:
        - name
      properties:
        name:
          type: string

    Device:
      allOf:
        - $ref: "#/components/schemas/WithId"
        - $ref: "#/components/schemas/WithName"</code></code></pre><p><code>allOf</code>, <code>oneOf</code> and <code>anyOf</code> are very powerful techniques for modelling your OAS.</p><h3>Extensions</h3><p>OpenAPI schema can be extended with internal properties that do not affect the schema itself, but are useful for server or client generators. A good example is our <a href="https://github.com/ulid/spec">ULID</a> type for ids:</p><pre><code><code>ULID:
  type: string
  minLength: 26
  maxLength: 26

  # example is useful for Swagger docs
  example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

  x-go-type: ulid.ULID
  x-go-type-import:
    path: github.com/oklog/ulid/v2</code></code></pre><p>The <code>x-</code> props will be used by Go server generator to use existing Go types for this field instead of generating a new one.</p><h2>Generate a Go Server</h2><p>We didn't go through all possible schema properties and just covered the main ones, so people who are not familiar with OAS have a good understanding of this standard. You can read the whole specification <a href="https://spec.openapis.org/oas/latest.html">here</a>. But now as our schema is ready we can generate a Go server from it.</p><p>You can find the full list of generators on <a href="https://openapi.tools/">opeanapi.tools</a>, there are a lot of them. However, the most popular one for Go servers is <a href="https://github.com/oapi-codegen/oapi-codegen">oapi-codegen</a>.</p><blockquote><p>oapi-codegen currently doesn&#8217;t support OAS 3.1. <a href="https://github.com/oapi-codegen/oapi-codegen/issues/373">Issue</a></p><p><a href="https://github.com/ogen-go/ogen/">ogen</a> does, however.</p></blockquote><p>You can install it via <code>go install</code>:</p><pre><code><code>go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest</code></code></pre><p>The configuration for the <code>oapi-codegen</code> generator is straightforward. You can either provide command line arguments or specify the same arguments in a yaml configuration file. You can choose which HTTP router to use for the server, where to put the output file, and more. In our case let's use <a href="https://github.com/labstack/echo">echo</a> router.</p><pre><code><code># oapi-codegen.yaml

package: api
output: pkg/api/api.gen.go

generate:
  strict-server: true
  models: true
  echo-server: true</code></code></pre><p>We can now generate the server code using the following command:</p><pre><code><code>oapi-codegen --config=oapi-codegen.yaml openapi.yaml</code></code></pre><p>Let's explore now the generated <code>api.gen.go</code> file. It contains the following parts.</p><p>Since we enabled <code>strict-server</code>, which will generate code that parses request bodies and encodes responses automatically, the interface that we need to implement is called <code>StrictServerInterface</code>:</p><pre><code><code>type StrictServerInterface interface {

  // List Devices
  // (GET /devices)
  ListDevices(ctx context.Context, request ListDevicesRequestObject) (ListDevicesResponseObject, error)

  // Get Device
  // (GET /devices/{deviceId})
  GetDevice(ctx context.Context, request GetDeviceRequestObject) (GetDeviceResponseObject, error)

}</code></code></pre><p>All our types are also generated:</p><pre><code><code>type ULID = ulid.ULID

type Device struct {
&#9;Id   ULID   `json:"id"`
&#9;Name string `json:"name"`
}

// ...</code></code></pre><p>As well as:</p><ul><li><p>Code to parse the requests automatically</p></li><li><p>Swagger definition</p></li></ul><h3>Implementation</h3><p>What's left for us to do is to create a server using echo, implement the generated interface and glue everything together. We can write the following code in <code>pkg/api/impl.go</code>:</p><pre><code><code>package api

import "context"

type Server struct{}

func NewServer() Server {
&#9;return Server{}
}

func (Server) ListDevices(ctx context.Context, request ListDevicesRequestObject) (ListDevicesResponseObject, error) {
&#9;// actual implementation
&#9;return ListDevices200JSONResponse{}, nil
}

func (Server) GetDevice(ctx context.Context, request GetDeviceRequestObject) (GetDeviceResponseObject, error) {
&#9;// actual implementation
&#9;return GetDevice200JSONResponse{}, nil
}</code></code></pre><p>I skipped the implementation part and just demonstrated how to return the responses, it's quite handy that <code>oapi-codegen</code> generated all possible responses for us.</p><p>That leaves us to start the echo server itself. Note that we don't need to write any endpoints manually now, and all request and response parsing is handled for us. Still, we need to validate the requests inside our implementation.</p><pre><code><code>package main

import (
&#9;"oapiexample/pkg/api"

&#9;"github.com/labstack/echo/v4"
)

func main() {
&#9;server := api.NewServer()

&#9;e := echo.New()

&#9;api.RegisterHandlers(e, api.NewStrictHandler(
&#9;&#9;server,
&#9;&#9;// add middlewares here if needed
&#9;&#9;[]api.StrictMiddlewareFunc{},
&#9;))

&#9;e.Start("127.0.0.1:8080")
}</code></code></pre><p>Now when we run our server using <code>go run .</code> we can curl <code>localhost:8080/devices</code> to se the response!</p><h3>Supported servers</h3><p><code>oapi-codegen</code> supports many web frameworks/servers, such as Chi, Fiber, Gin as well as standard <code>net/http</code>.</p><h3>Swagger UI / Postman</h3><p>Sometimes it's handy to have Swagger docs shipped together with your API, for testing for example or just as public documentation. <code>oapi-codegen</code> doesn't generate the Swagger UI out of the box but we can have a simple html page that has a Swagger JS which loads our OAS.</p><p>You can find the html code for our <code>pkg/api/index.html</code> <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/">here</a>.</p><p>And then we can use <code>go:embed</code> to embed the static files and add our swagger endpoint:</p><pre><code><code>//go:embed pkg/api/index.html
//go:embed openapi.yaml
var swaggerUI embed.FS

func main() {
&#9;// ...

&#9;// serve swagger docs
&#9;e.GET("/swagger/*", echo.WrapHandler(http.StripPrefix("/swagger/", http.FileServer(http.FS(swaggerUI)))))
}</code></code></pre><p>Now we can visit <code>localhost:8080/swagger/</code> to see the Swagger UI with our OAS.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KnKZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KnKZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 424w, https://substackcdn.com/image/fetch/$s_!KnKZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 848w, https://substackcdn.com/image/fetch/$s_!KnKZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 1272w, https://substackcdn.com/image/fetch/$s_!KnKZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KnKZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png" width="1092" height="922" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:922,&quot;width&quot;:1092,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:96697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KnKZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 424w, https://substackcdn.com/image/fetch/$s_!KnKZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 848w, https://substackcdn.com/image/fetch/$s_!KnKZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 1272w, https://substackcdn.com/image/fetch/$s_!KnKZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d35f8f-e1e7-4e51-9149-e180bb192fd8_1092x922.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Tools like Postman are very popular for API documentation, and it's also possible to <a href="http://learning.postman.com/docs/integrations/available-integrations/working-with-openAPI/">import</a> your existing OpenAPI 3.0 and 3.1 definitions into Postman. Postman supports both YAML and JSON formats.</p><h3>Generate OAS from code</h3><p>There is also a practice to generate OpenAPI schema from code, especially in typed languages. This approach has been popular, with the main selling point that keeping OpenAPI schema near the code will hopefully mean developers keep it up to date as they work on the code. This is not always the case, which is one of a few reasons this practice is dying out. And I am also not a big fan of that as I haven&#8217;t seen a big value in this. Anyway, you can have a look at the following projects: <a href="https://github.com/go-swagger/go-swagger">go-swagger</a>, <a href="https://github.com/swaggo/swag">swag</a>, <a href="https://github.com/swaggest/rest/">swaggest/rest</a>.</p><h2>Client Code</h2><p>As mentioned earlier, OpenAPI is very powerful for collaboration between teams, and all you have to do now is to properly version your schema (see <code>info.version</code> part) and distribute it across the teams. This part can be automated to some extent by packaging your OpenAPI schema and making it available. I've seen that devs use git submodules for that or GitHub actions to publish the version schemas.</p><p>Let's assume our client is a web application written in Typescript, which is quite common for web APIs. Again, there are may generators available at <a href="https://openapi.tools/">opeanapi.tools</a> online but the most popular one is [<a href="https://openapi-ts.dev/">openapi-typescript</a>.</p><p>Here's how you can generate the Typescript code for local or remote schemas:</p><pre><code><code># Local schema
npx openapi-typescript openapi.yaml -o ./client/schema.d.ts

# Remote schema
npx openapi-typescript https://.../openapi.yaml -o ./client/schema.d.ts</code></code></pre><h2>Conclusion</h2><p>OpenAPI is a de-facto standard for designing, implementing and consuming the REST APIs, so it's crucial to understand how it works. I hope this article has provided a useful introduction to OpenAPI Specification, as well as practical tips and examples for how to use OAS to architect, implement and consume APIs.</p><h2>Resources</h2><ul><li><p><a href="https://www.youtube.com/watch?v=87au30fl5e4">Watch on YouTube</a></p></li><li><p><a href="https://github.com/plutov/packagemain/tree/master/oapi-example">Source code</a></p></li><li><p><a href="https://www.openapis.org/">OpenAPI Initiative</a></p></li><li><p><a href="https://openapi.tools/">openapi.tools</a></p></li><li><p><a href="https://editor.swagger.io/">Swagger Editor</a></p></li><li><p><a href="https://github.com/oapi-codegen/oapi-codegen">oapi-codegen</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Mastering cross-database operations with PostgreSQL FDW ]]></title><description><![CDATA[A guide to schema import, archival policies, and automation]]></description><link>https://packagemain.tech/p/mastering-cross-database-operations-with-postgres-fdw</link><guid isPermaLink="false">https://packagemain.tech/p/mastering-cross-database-operations-with-postgres-fdw</guid><dc:creator><![CDATA[Julien Singler]]></dc:creator><pubDate>Sat, 08 Feb 2025 17:11:22 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/df040655-35b4-4034-84d9-6aa8d83756f9_1100x790.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>Modern applications often rely on data scattered across multiple databases. <a href="https://wiki.postgresql.org/wiki/Foreign_data_wrappers">PostgreSQL&#8217;s </a><strong><a href="https://wiki.postgresql.org/wiki/Foreign_data_wrappers">Foreign Data Wrapper (FDW)</a></strong> empowers you to query and manipulate external databases as if they were native PostgreSQL tables. Combined with <strong>pg_cron</strong> for scheduling and stored procedures for automation, you can build robust archival pipelines.</p><p>In this post we will talk about:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">packagemain.tech is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><ul><li><p>Setting up FDW to connect to external databases (ex: another PostgreSQL instance).</p></li><li><p>Importing foreign schemas.</p></li><li><p>Creating archival policies with stored procedures</p></li><li><p>Automating archival tasks with pg_cron</p></li></ul><h2>What is PostgreSQL FDW?</h2><p>FDW is an extension that implements the SQL/MED standard, allowing PostgreSQL to interact with external data sources.</p><p>Key benefits: </p><ul><li><p><strong>Unified Query Interface:</strong> Run SQL across heterogenous databases (<a href="https://github.com/EnterpriseDB/mysql_fdw">MySQL</a>, <a href="https://github.com/EnterpriseDB/mongo_fdw">MongoDB</a>, <a href="https://github.com/EnterpriseDB/hdfs_fdw">Hadoop</a> etc..) </p></li><li><p><strong>Real-Time data access:</strong> No need for ETL pipelines for simple joins.</p></li><li><p><strong>Simplified Archival:</strong> Move historical data to cheaper storage while keeping it queryable.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fzQS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fzQS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 424w, https://substackcdn.com/image/fetch/$s_!fzQS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 848w, https://substackcdn.com/image/fetch/$s_!fzQS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 1272w, https://substackcdn.com/image/fetch/$s_!fzQS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fzQS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png" width="1100" height="790" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:790,&quot;width&quot;:1100,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:74895,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fzQS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 424w, https://substackcdn.com/image/fetch/$s_!fzQS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 848w, https://substackcdn.com/image/fetch/$s_!fzQS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 1272w, https://substackcdn.com/image/fetch/$s_!fzQS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe34ec621-ff5e-457a-bcbc-da01d93d2d59_1100x790.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Foreign Data Wrappers</h2><p>There is a lot of different FDW extensions that will let you connect to a variety of databases (relational, no-sql, files etc..)</p><ul><li><p><a href="https://www.postgresql.org/docs/current/postgres-fdw.html">postgres_fdw</a></p><ul><li><p>Connects to other PostgreSQL databases (local or remote)</p></li><li><p>Supports read/write operations</p></li></ul></li><li><p><a href="https://docs.postgresql.fr/17/file-fdw.html">file_fdw</a></p><ul><li><p>Reads data from flat files (CSV, TSV, etc..)</p></li></ul></li><li><p><a href="https://github.com/EnterpriseDB/mysql_fdw">mysql_fdw</a></p><ul><li><p>Connects to MySQL/MariaDB databases</p></li><li><p>Supports basic queries and joins</p></li></ul></li><li><p><a href="https://github.com/pg-redis-fdw/redis_fdw">redis_fdw</a></p><ul><li><p>Query Redis key-value stores</p></li></ul></li><li><p><a href="https://github.com/EnterpriseDB/mongo_fdw">mongo_fdw</a></p><ul><li><p>Access MongoDB collections</p></li></ul></li><li><p><a href="https://github.com/umitanuki/s3_fdw">aws_s3_fdw</a></p><ul><li><p>Read/write data from Amazon S3</p></li></ul></li></ul><p>There are many others, which you can find <a href="https://wiki.postgresql.org/wiki/Foreign_data_wrappers">here</a>.</p><h2>Use cases</h2><p>As you can imagine, there are many use cases possible. </p><p>From reading files from your database, to getting cache keys from Redis and augment them from content stored in your PostgreSQL.</p><p><strong>The use case we will focus on, is an automatic archival from PostgreSQL to PostgreSQL.</strong> But it would also work from PostgreSQL to Hadoop | MySQL | Bigquery etc..</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z0DQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 424w, https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 848w, https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 1272w, https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png" width="1456" height="935" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:935,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116495,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 424w, https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 848w, https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 1272w, https://substackcdn.com/image/fetch/$s_!Z0DQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d9b702-8999-4008-ac3e-30572f325eba_1464x940.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setting up FDW and connecting to a foreign database</h2><h3><em>Step 1: Install required extension</em></h3><pre><code>-- Enable FDW   
CREATE EXTENSION IF NOT EXISTS postgres_fdw;</code></pre><h3><em>Step 2: configure a foreign server (ex: PostgreSQL) </em></h3><pre><code>-- Create a foreign server
CREATE SERVER postgres_server FOREIGN DATA WRAPPER postgres_fdw
   OPTIONS (host '{FOREIGN_HOST}', dbname '{FOREIGN_DB_NAME}');

-- Map local PostgreSQL user to foreign server credentials
CREATE USER MAPPING FOR postgres SERVER postgres_server
   OPTIONS (user '{FOREIGN_USERNAME}', password '{FOREIGN_PASSWORD}');</code></pre><h3>Step 3: <em>Import foreign schema</em></h3><p>You might be tempted to import foreign schema from the other postgres into the &#8220;public&#8221; schema of your current instance, however you can&#8217;t have 2 tables with the same name. </p><p>That&#8217;s why, I would suggest you create a separate schema, you can call it foreign_schema or whatever you wish:</p><pre><code>-- Create a new schema to import the tables into
CREATE SCHEMA foreign_schema;

-- Import all tables from the foreign postgreSQL database
IMPORT FOREIGN SCHEMA public FROM SERVER postgres_server INTO foreign_scehma;

-- You can also import only specific tables
IMPORT FOREIGN SCHEMA public LIMIT TO ({TABLE1}, {TABLE2}) FROM SERVER postgres_server INTO foreign_schema;</code></pre><p>Now you can query your foreign database:</p><pre><code>SELECT * FROM foreign_schema.{TABLE1};</code></pre><h2>Building an archival policy</h2><p>Let&#8217;s imagine you have a <code>transactions</code> table in both your postgres with the same columns (though it&#8217;s not mandatory).</p><p>The distant server is used as archive and the current db (the one you are connected to) is your live DB. </p><pre><code>CREATE TABLE IF NOT EXISTS transactions (
    id uuid DEFAULT uuid_generate_v4(),
    amount INT DEFAULT 0,
    created_at   TIMESTAMPTZ DEFAULT NOW(),

    PRIMARY KEY (id)
); </code></pre><h3><em>Create a stored procedure to move data</em></h3><pre><code>CREATE OR REPLACE PROCEDURE archive_old_transactions()  
LANGUAGE plpgsql  
AS $$  
BEGIN  
  -- Move data older than 1 year to archive  
  INSERT INTO foreign_schema.transactions  
  SELECT * FROM public.transactions  
  WHERE created_at &lt; NOW() - INTERVAL '1 year';  

  -- Delete archived data from main table  
  DELETE FROM public.transactions  
  WHERE created_at &lt; NOW() - INTERVAL '1 year';  
END;  
$$
;  </code></pre><h2>Automate archival with pg_cron</h2><h3><em>Step1: Install required extension:</em></h3><pre><code>-- Enable pg cron   
CREATE EXTENSION IF NOT EXISTS pg_cron;</code></pre><h3><em>Step 2: Schedule the stored procedure to run nightly:</em></h3><pre><code>-- Run at 2 AM daily
SELECT cron.schedule(
  'archive_transactions',
  '0 2 * * *',
  'CALL archive_old_transactions()'
); </code></pre><h3><em>Step 3: Create a view to merge both worlds</em></h3><pre><code>CREATE VIEW combined_transactions AS (
    WITH remote_data AS (
      SELECT * FROM foreign_schema.transactions
    ),
    local_data AS (
      SELECT * FROM public.transactions
    )
    SELECT * FROM remote_data
    UNION ALL
    SELECT * FROM local_data
);</code></pre><p>You will notice that I didn&#8217;t simply do a UNION of two tables, I used CTE (Common Table Expressions) because it is crucial for optimizing queries with foreign tables. </p><p>Essentially, it containerizes the FDW query, because the query planner will have to ask the foreign database to execute its part, and the clearer this query is, the faster it will be.</p><p>Best approach is to:</p><ul><li><p>Containerize the FDW query to reduce data transfer</p></li><li><p>Filter data at the source </p></li><li><p>Minimize the returned row count </p></li></ul><h3>Indexes &amp; foreign tables</h3><p>Nothing forces you to have the exact same copy of foreign and local table, neither you have to force foreign keys or equivalent indexes. </p><p>It is probably recommended to create specific indexes in your foreign table that will match the query patterns they will be submitted to. Because you don&#8217;t query archive data the same way you might query live data. </p><h3>Materialized Views</h3><p>The concept of archival is often to reclaim space, but if you were to need faster access to data from both the archive and the live DB then you could use Materialized views to ensure fast queries. </p><h3><em>Step 4: Query your view </em></h3><p>So if you query transactions from 2 years ago from now, until now. You necessarily have to query both tables.</p><pre><code>SELECT * FROM combined_transactions WHERE created_at BETWEEN(NOW() - INTERVAL '2 year', NOW()); </code></pre><h2><strong>Best Practices</strong></h2><ol><li><p><strong>Index Foreign Tables</strong>: Improve query performance with indexes on frequently filtered columns.</p></li><li><p><strong>Batch Archival</strong>: Use <code>LIMIT</code> in procedures to avoid long locks.</p></li><li><p><strong>Error Handling</strong>: Wrap pg_cron jobs in <code>BEGIN...EXCEPTION</code> blocks.</p></li><li><p><strong>Monitor pg_cron</strong>: Use <code>cron.job_run_details</code> to track job history.</p></li></ol><h2><strong>Conclusion</strong></h2><p>PostgreSQL FDW transforms your database into a unified gateway for cross-database operations. By combining it with pg_cron and stored procedures, you can automate complex workflows like archival, reporting, and data synchronization without external tools. </p><h3><strong>Further Reading</strong>:</h3><ul><li><p><a href="https://www.postgresql.org/docs/current/postgres-fdw.html">PostgreSQL FDW Documentation</a></p></li><li><p><a href="https://github.com/citusdata/pg_cron">pg_cron GitHub Repository</a></p></li><li><p><a href="https://wiki.postgresql.org/wiki/Foreign_data_wrappers">FDW Wrappers for MongoDB, CSV, etc.</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Essential CLI/TUI Tools for Developers]]></title><description><![CDATA[An opinionated list of CLI/TUI applications for developer productivity.]]></description><link>https://packagemain.tech/p/essential-clitui-tools-for-developers</link><guid isPermaLink="false">https://packagemain.tech/p/essential-clitui-tools-for-developers</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Mon, 06 Jan 2025 19:39:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/aa152021-91be-4da8-89e3-51ff69b5a832_2320x1496.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div id="youtube2-hsPzLalRnpc" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;hsPzLalRnpc&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/hsPzLalRnpc?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>We, developers, spend a lot of time in our terminal. Or maybe we could spend even more, because there are so many great CLI/TUI tools that can boost the developer productivity, or just be fun to use.</p><p>This article contains a categorized list of CLIs / TUIs I personally use and those widely adopted in the development community.</p><h2>Kubernetes</h2><p><strong><a href="https://github.com/derailed/k9s">k9s</a></strong> - Kubernetes CLI To Manage Your Clusters In Style!</p><blockquote><p>K9s provides a terminal UI to interact with your Kubernetes clusters. The aim of this project is to make it easier to navigate, observe and manage your applications in the wild. K9s continually watches Kubernetes for changes and offers subsequent commands to interact with your observed resources.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sWwI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sWwI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 424w, https://substackcdn.com/image/fetch/$s_!sWwI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 848w, https://substackcdn.com/image/fetch/$s_!sWwI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 1272w, https://substackcdn.com/image/fetch/$s_!sWwI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sWwI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png" width="1456" height="763" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:763,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1114105,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sWwI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 424w, https://substackcdn.com/image/fetch/$s_!sWwI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 848w, https://substackcdn.com/image/fetch/$s_!sWwI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 1272w, https://substackcdn.com/image/fetch/$s_!sWwI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8cd389-3436-46ba-b83e-8df8511aab1a_2520x1320.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/vladimirvivien/ktop">ktop</a></strong> - A top-like tool for your Kubernetes clusters.</p><blockquote><p>Following the tradition of Unix/Linux top tools, ktop is a tool that displays useful metrics information about nodes, pods, and other workload resources running in a Kubernetes cluster.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-ksb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-ksb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 424w, https://substackcdn.com/image/fetch/$s_!-ksb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 848w, https://substackcdn.com/image/fetch/$s_!-ksb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 1272w, https://substackcdn.com/image/fetch/$s_!-ksb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-ksb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png" width="1456" height="749" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a3712145-f481-407c-a233-35b074b90559_2640x1358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:749,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:784426,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-ksb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 424w, https://substackcdn.com/image/fetch/$s_!-ksb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 848w, https://substackcdn.com/image/fetch/$s_!-ksb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 1272w, https://substackcdn.com/image/fetch/$s_!-ksb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712145-f481-407c-a233-35b074b90559_2640x1358.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/ahmetb/kubectx">kubectx</a></strong> - switch between contexts (clusters) on kubectl faster.</p><blockquote><p>kubectx is a tool to switch between contexts (clusters) on kubectl faster.</p><p>kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8FaK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8FaK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 424w, https://substackcdn.com/image/fetch/$s_!8FaK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 848w, https://substackcdn.com/image/fetch/$s_!8FaK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 1272w, https://substackcdn.com/image/fetch/$s_!8FaK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8FaK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif" width="1367" height="472" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:472,&quot;width&quot;:1367,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:166708,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8FaK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 424w, https://substackcdn.com/image/fetch/$s_!8FaK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 848w, https://substackcdn.com/image/fetch/$s_!8FaK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 1272w, https://substackcdn.com/image/fetch/$s_!8FaK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567ff2cb-8347-41f2-b70a-175b7720377e_1367x472.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/kubescape/kubescape">kubescape</a></strong> - Kubernetes security platform for your IDE, CI/CD pipelines, and clusters.</p><blockquote><p>Kubescape is an open-source Kubernetes security platform that provides comprehensive security coverage, from left to right across the entire development and deployment lifecycle. It offers hardening, posture management, and runtime security capabilities to ensure robust protection for Kubernetes environments. It saves Kubernetes users and admins precious time, effort, and resources.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tVWN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tVWN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 424w, https://substackcdn.com/image/fetch/$s_!tVWN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 848w, https://substackcdn.com/image/fetch/$s_!tVWN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 1272w, https://substackcdn.com/image/fetch/$s_!tVWN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tVWN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png" width="1456" height="1157" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1157,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:334793,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tVWN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 424w, https://substackcdn.com/image/fetch/$s_!tVWN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 848w, https://substackcdn.com/image/fetch/$s_!tVWN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 1272w, https://substackcdn.com/image/fetch/$s_!tVWN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd5fa171-6ac2-4358-940f-108dc22f7f21_1858x1476.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Containers</h2><p><strong><a href="https://github.com/bcicen/ctop">ctop</a></strong> - A top-like interface for container metrics.</p><blockquote><p>ctop provides a concise and condensed overview of real-time metrics for multiple containers.</p><p>ctop comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!i5Cs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!i5Cs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 424w, https://substackcdn.com/image/fetch/$s_!i5Cs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 848w, https://substackcdn.com/image/fetch/$s_!i5Cs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 1272w, https://substackcdn.com/image/fetch/$s_!i5Cs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!i5Cs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif" width="1195" height="414" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:414,&quot;width&quot;:1195,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:691694,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!i5Cs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 424w, https://substackcdn.com/image/fetch/$s_!i5Cs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 848w, https://substackcdn.com/image/fetch/$s_!i5Cs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 1272w, https://substackcdn.com/image/fetch/$s_!i5Cs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ff82c38-66d2-4808-be0c-998a483be7f5_1195x414.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/jesseduffield/lazydocker">lazydocker</a></strong> - A simple terminal UI for both docker and docker-compose.</p><blockquote><p>Memorising docker commands is hard. Memorising aliases is slightly less hard. Keeping track of your containers across multiple terminal windows is near impossible. What if you had all the information you needed in one terminal window with every common command living one keypress away (and the ability to add custom commands as well). Lazydocker's goal is to make that dream a reality.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HTK1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HTK1!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 424w, https://substackcdn.com/image/fetch/$s_!HTK1!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 848w, https://substackcdn.com/image/fetch/$s_!HTK1!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 1272w, https://substackcdn.com/image/fetch/$s_!HTK1!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HTK1!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5447632,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HTK1!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 424w, https://substackcdn.com/image/fetch/$s_!HTK1!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 848w, https://substackcdn.com/image/fetch/$s_!HTK1!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 1272w, https://substackcdn.com/image/fetch/$s_!HTK1!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4cdcb11-bf3c-4d75-a51d-40bf62cdf287_1706x960.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong><a href="https://github.com/wagoodman/dive">dive</a></strong> - A tool for exploring each layer in a docker image.</p><blockquote><p>A tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mwqL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mwqL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 424w, https://substackcdn.com/image/fetch/$s_!mwqL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 848w, https://substackcdn.com/image/fetch/$s_!mwqL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 1272w, https://substackcdn.com/image/fetch/$s_!mwqL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mwqL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1910463,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mwqL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 424w, https://substackcdn.com/image/fetch/$s_!mwqL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 848w, https://substackcdn.com/image/fetch/$s_!mwqL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 1272w, https://substackcdn.com/image/fetch/$s_!mwqL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f9499be-7753-4f00-ad20-bd1df5b6f77c_1734x1083.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Files/Text</h2><p><strong><a href="https://github.com/jqlang/jq">jq</a></strong> - Command-line JSON processor.</p><blockquote><p>jq is a lightweight and flexible command-line JSON processor akin to sed, awk, grep, and friends for JSON data. It's written in portable C and has zero runtime dependencies, allowing you to easily slice, filter, map, and transform structured data.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u50W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u50W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 424w, https://substackcdn.com/image/fetch/$s_!u50W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 848w, https://substackcdn.com/image/fetch/$s_!u50W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 1272w, https://substackcdn.com/image/fetch/$s_!u50W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u50W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png" width="747" height="350" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:350,&quot;width&quot;:747,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:57986,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!u50W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 424w, https://substackcdn.com/image/fetch/$s_!u50W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 848w, https://substackcdn.com/image/fetch/$s_!u50W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 1272w, https://substackcdn.com/image/fetch/$s_!u50W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030f1d2b-8c52-415f-b0d2-ad713d96b1a2_747x350.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/sharkdp/bat">bat</a></strong> - A cat(1) clone with wings.</p><blockquote><p>bat is an enhanced version of the cat command, written in Rust, integrating syntax highlighting, git integration, and automatic paging. </p><p>Its syntax highlighting supports numerous programming and markup languages, making code more readable directly in the terminal. Git integration allows users to see modifications in relation to the index, highlighting the added or changed lines. </p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HgL2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HgL2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 424w, https://substackcdn.com/image/fetch/$s_!HgL2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 848w, https://substackcdn.com/image/fetch/$s_!HgL2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 1272w, https://substackcdn.com/image/fetch/$s_!HgL2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HgL2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png" width="656" height="450" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:450,&quot;width&quot;:656,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:38814,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HgL2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 424w, https://substackcdn.com/image/fetch/$s_!HgL2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 848w, https://substackcdn.com/image/fetch/$s_!HgL2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 1272w, https://substackcdn.com/image/fetch/$s_!HgL2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4e806c2-cd8b-4727-82c2-a5497f5fc8f6_656x450.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/BurntSushi/ripgrep">ripgrep</a></strong> - Recursively search directories for a regex pattern while respecting your gitignore.</p><blockquote><p>ripgrep is a line-oriented search tool that recursively searches the current directory for a regex pattern. By default, ripgrep will respect gitignore rules and automatically skip hidden files/directories and binary files. (To disable all automatic filtering by default, use rg -uuu.) ripgrep has first class support on Windows, macOS and Linux, with binary downloads available for every release. ripgrep is similar to other popular search tools like The Silver Searcher, ack and grep.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!84Fy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!84Fy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 424w, https://substackcdn.com/image/fetch/$s_!84Fy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 848w, https://substackcdn.com/image/fetch/$s_!84Fy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 1272w, https://substackcdn.com/image/fetch/$s_!84Fy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!84Fy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp" width="1396" height="744" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:744,&quot;width&quot;:1396,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:31622,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!84Fy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 424w, https://substackcdn.com/image/fetch/$s_!84Fy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 848w, https://substackcdn.com/image/fetch/$s_!84Fy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 1272w, https://substackcdn.com/image/fetch/$s_!84Fy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07b34b4e-14d3-4155-b380-501d25dcf5e6_1396x744.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/RsyncProject/rsync">rsync</a></strong> - A fast and extraordinarily versatile file copying tool for both remote and local files.</p><blockquote><p>Rsync uses a delta-transfer algorithm which provides a very fast method for bringing remote files into sync. It does this by sending just the differences in the files across the link, without requiring that both sets of files are present at one of the ends of the link beforehand. At first glance this may seem impossible because the calculation of diffs between two files normally requires local access to both files.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KVwV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KVwV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 424w, https://substackcdn.com/image/fetch/$s_!KVwV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 848w, https://substackcdn.com/image/fetch/$s_!KVwV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 1272w, https://substackcdn.com/image/fetch/$s_!KVwV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KVwV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png" width="1456" height="1064" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1064,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:812008,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KVwV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 424w, https://substackcdn.com/image/fetch/$s_!KVwV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 848w, https://substackcdn.com/image/fetch/$s_!KVwV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 1272w, https://substackcdn.com/image/fetch/$s_!KVwV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F940324e2-3a36-45ff-9625-b44c772b027e_3680x2688.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/sharkdp/fd">fd</a></strong> - A simple, fast and user-friendly alternative to find.</p><blockquote><p>fd is a program to find entries in your filesystem. It is a simple, fast and user-friendly alternative to find. While it does not aim to support all of find's powerful functionality, it provides sensible (opinionated) defaults for a majority of use cases.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jRpH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jRpH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 424w, https://substackcdn.com/image/fetch/$s_!jRpH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 848w, https://substackcdn.com/image/fetch/$s_!jRpH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 1272w, https://substackcdn.com/image/fetch/$s_!jRpH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jRpH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png" width="608" height="448" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:448,&quot;width&quot;:608,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44393,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jRpH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 424w, https://substackcdn.com/image/fetch/$s_!jRpH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 848w, https://substackcdn.com/image/fetch/$s_!jRpH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 1272w, https://substackcdn.com/image/fetch/$s_!jRpH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f3691a-f25a-48f0-9cb9-a727f15b3374_608x448.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Git</h2><p><strong><a href="https://github.com/jesseduffield/lazygit">lazygit</a></strong> - Simple terminal UI for git commands.</p><blockquote><p>Lazygit is a simple terminal UI for git commands, with keybindings for most actions. It aims to make working with git from the terminal easier and more intuitive.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LYFa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LYFa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 424w, https://substackcdn.com/image/fetch/$s_!LYFa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 848w, https://substackcdn.com/image/fetch/$s_!LYFa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 1272w, https://substackcdn.com/image/fetch/$s_!LYFa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LYFa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png" width="956" height="521" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:521,&quot;width&quot;:956,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94195,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LYFa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 424w, https://substackcdn.com/image/fetch/$s_!LYFa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 848w, https://substackcdn.com/image/fetch/$s_!LYFa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 1272w, https://substackcdn.com/image/fetch/$s_!LYFa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48a08bad-a2f9-4379-a2d1-2564f6d8234e_956x521.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Development</h2><p><strong><a href="https://github.com/Julien-cpsn/ATAC">ATAC</a></strong> - A simple API client (postman like) in your terminal.</p><blockquote><p>ATAC is Arguably a Terminal API Client. It is based on well-known clients such as Postman, Insomnia, or even Bruno, but inside your terminal without any specific graphical environment needed.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CTjs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CTjs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 424w, https://substackcdn.com/image/fetch/$s_!CTjs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 848w, https://substackcdn.com/image/fetch/$s_!CTjs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 1272w, https://substackcdn.com/image/fetch/$s_!CTjs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CTjs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png" width="1456" height="783" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:783,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:266673,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CTjs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 424w, https://substackcdn.com/image/fetch/$s_!CTjs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 848w, https://substackcdn.com/image/fetch/$s_!CTjs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 1272w, https://substackcdn.com/image/fetch/$s_!CTjs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff35057de-2653-487b-9f0d-cae23f3fb187_1898x1021.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/grafana/k6">k6</a></strong> - A modern load testing tool, using Go and JavaScript.</p><blockquote><p>k6 is a modern load-testing tool, built on our years of experience in the performance and testing industries. It's built to be powerful, extensible, and full-featured. The key design goal is to provide the best developer experience.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6pk5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6pk5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 424w, https://substackcdn.com/image/fetch/$s_!6pk5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 848w, https://substackcdn.com/image/fetch/$s_!6pk5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 1272w, https://substackcdn.com/image/fetch/$s_!6pk5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6pk5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png" width="1456" height="1189" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1189,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:55760,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6pk5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 424w, https://substackcdn.com/image/fetch/$s_!6pk5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 848w, https://substackcdn.com/image/fetch/$s_!6pk5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 1272w, https://substackcdn.com/image/fetch/$s_!6pk5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08a6eff9-a367-40c2-9f99-9193b3df9ba9_1582x1292.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/httpie/cli">httpie</a></strong> - modern, user-friendly command-line HTTP client for the API era.</p><blockquote><p>HTTPie (pronounced aitch-tee-tee-pie) is a command-line HTTP client. Its goal is to make CLI interaction with web services as human-friendly as possible. HTTPie is designed for testing, debugging, and generally interacting with APIs &amp; HTTP servers. The http &amp; https commands allow for creating and sending arbitrary HTTP requests. They use simple and natural syntax and provide formatted and colorized output.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iahK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iahK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 424w, https://substackcdn.com/image/fetch/$s_!iahK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 848w, https://substackcdn.com/image/fetch/$s_!iahK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 1272w, https://substackcdn.com/image/fetch/$s_!iahK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iahK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif" width="1024" height="512" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:512,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1043348,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iahK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 424w, https://substackcdn.com/image/fetch/$s_!iahK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 848w, https://substackcdn.com/image/fetch/$s_!iahK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 1272w, https://substackcdn.com/image/fetch/$s_!iahK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb786e946-537d-41c1-a6bd-d0e5d9ff0746_1024x512.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/asciinema/asciinema">asciinema</a></strong> - Terminal session recorder.</p><blockquote><p>asciinema (aka asciinema CLI or asciinema recorder) is a command-line tool for recording terminal sessions.</p><p>Unlike typical screen recording software, which records visual output of a screen into a heavyweight video files (.mp4, .mov), asciinema recorder runs inside a terminal, capturing terminal session output into a lightweight recording files in the asciicast format (.cast).</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PvMp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PvMp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 424w, https://substackcdn.com/image/fetch/$s_!PvMp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 848w, https://substackcdn.com/image/fetch/$s_!PvMp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 1272w, https://substackcdn.com/image/fetch/$s_!PvMp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PvMp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png" width="1397" height="777" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:777,&quot;width&quot;:1397,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:105168,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PvMp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 424w, https://substackcdn.com/image/fetch/$s_!PvMp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 848w, https://substackcdn.com/image/fetch/$s_!PvMp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 1272w, https://substackcdn.com/image/fetch/$s_!PvMp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fadf80-31cb-4951-a2aa-5f5e8f66b8d6_1397x777.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Networking</h2><p><strong><a href="https://github.com/mr-karan/doggo">doggo</a></strong> - Command-line DNS Client for Humans.</p><blockquote><p>doggo is a modern command-line DNS client (like dig) written in Golang. It outputs information in a neat concise manner and supports protocols like DoH, DoT, DoQ, and DNSCrypt as well.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tzRB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tzRB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 424w, https://substackcdn.com/image/fetch/$s_!tzRB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 848w, https://substackcdn.com/image/fetch/$s_!tzRB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 1272w, https://substackcdn.com/image/fetch/$s_!tzRB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tzRB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png" width="1456" height="1018" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1018,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1761266,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tzRB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 424w, https://substackcdn.com/image/fetch/$s_!tzRB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 848w, https://substackcdn.com/image/fetch/$s_!tzRB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 1272w, https://substackcdn.com/image/fetch/$s_!tzRB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc58db052-5a05-41f9-b30d-af2e79d09b04_3680x2572.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong><a href="https://github.com/orf/gping">gping</a></strong> - Ping, but with a graph.</p><blockquote><p>gping is an excellent tool for network, system, devops engineers and for anyone looking to visualize their ping output. It's best utilized in environments where readability and visual communication of ping results is essential.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!K_ce!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!K_ce!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 424w, https://substackcdn.com/image/fetch/$s_!K_ce!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 848w, https://substackcdn.com/image/fetch/$s_!K_ce!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 1272w, https://substackcdn.com/image/fetch/$s_!K_ce!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!K_ce!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1236707,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!K_ce!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 424w, https://substackcdn.com/image/fetch/$s_!K_ce!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 848w, https://substackcdn.com/image/fetch/$s_!K_ce!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 1272w, https://substackcdn.com/image/fetch/$s_!K_ce!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ad0facf-7c87-4da2-b460-f9f7668ef275_1411x757.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Workstation</h2><p><strong><a href="https://github.com/tmux/tmux/wiki">tmux</a></strong> - A terminal multiplexer.</p><blockquote><p>tmux is a terminal multiplexer. It enables a number of terminals to be created, accessed, and controlled from a single screen.</p><p>tmux may be detached from a screen and continue running in the background, then later reattached.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fYMv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fYMv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 424w, https://substackcdn.com/image/fetch/$s_!fYMv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 848w, https://substackcdn.com/image/fetch/$s_!fYMv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 1272w, https://substackcdn.com/image/fetch/$s_!fYMv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fYMv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png" width="1000" height="420" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:420,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:180813,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fYMv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 424w, https://substackcdn.com/image/fetch/$s_!fYMv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 848w, https://substackcdn.com/image/fetch/$s_!fYMv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 1272w, https://substackcdn.com/image/fetch/$s_!fYMv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62bbb11-3aeb-41b0-bd7a-97bddc6602c1_1000x420.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong><a href="https://github.com/zellij-org/zellij">zellij</a></strong> - A terminal workspace with batteries included.</p><blockquote><p>Zellij is a workspace aimed at developers, ops-oriented people and anyone who loves the terminal. Similar programs are sometimes called "Terminal Multiplexers".</p><p>Zellij is designed around the philosophy that one must not sacrifice simplicity for power, taking pride in its great experience out of the box as well as the advanced features it places at its users' fingertips.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uY33!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uY33!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 424w, https://substackcdn.com/image/fetch/$s_!uY33!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 848w, https://substackcdn.com/image/fetch/$s_!uY33!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 1272w, https://substackcdn.com/image/fetch/$s_!uY33!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uY33!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif" width="825" height="435" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:435,&quot;width&quot;:825,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4843386,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uY33!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 424w, https://substackcdn.com/image/fetch/$s_!uY33!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 848w, https://substackcdn.com/image/fetch/$s_!uY33!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 1272w, https://substackcdn.com/image/fetch/$s_!uY33!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed546b12-6a80-4764-8ddf-ff928e104dc0_825x435.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong><a href="https://github.com/aristocratos/btop">btop</a></strong> - A monitor of resources.</p><blockquote><p>Resource monitor that shows usage and stats for processor, memory, disks, network and processes.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vHvQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vHvQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 424w, https://substackcdn.com/image/fetch/$s_!vHvQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 848w, https://substackcdn.com/image/fetch/$s_!vHvQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 1272w, https://substackcdn.com/image/fetch/$s_!vHvQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vHvQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png" width="1045" height="658" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:658,&quot;width&quot;:1045,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:164431,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vHvQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 424w, https://substackcdn.com/image/fetch/$s_!vHvQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 848w, https://substackcdn.com/image/fetch/$s_!vHvQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 1272w, https://substackcdn.com/image/fetch/$s_!vHvQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef019389-6a49-4a7c-b2d9-2b9f43ebbe62_1045x658.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Conclusion</h2><p>These CLIs/TUIs should work well in any modern terminal, I personally use <a href="https://ghostty.org/">Ghostty</a> currently and it works great.</p><p>There's a huge amount of CLIs/TUIs out there, and we can't list them all (though we tried).</p>]]></content:encoded></item><item><title><![CDATA[Understanding the Language Server Protocol]]></title><description><![CDATA[LSP: the secret weapon of modern code editors.]]></description><link>https://packagemain.tech/p/understanding-the-language-server-protocol</link><guid isPermaLink="false">https://packagemain.tech/p/understanding-the-language-server-protocol</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Thu, 02 Jan 2025 15:54:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ef6cfe01-de32-4ede-a48a-70733d106d4c_1393x992.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the past, many code editors were built just for the specific language, and to provide rich and smart code editing, tight integration between the editor and the language tooling was a must. On the other hand, there were (and still are) more general-purpose editors, but they lacked in functionality when it came to more advanced language-specific features like code completion, &#8220;go to definition&#8221;, etc. (for example, code highlighting was done using the regular expressions).</p><p>With growing amount of code editors and programming languages this became the classic <strong>M*N</strong> complexity problem.</p><p>But then Microsoft introduced the <a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a> (LSP) as a solution to the problem above, which elegantly transforms this <strong>M*N</strong> complexity into a more manageable <strong>M+N</strong> situation.</p><blockquote><p><em>The LSP was initially driven by the needs of VS Code</em></p></blockquote><ul><li><p>LSP separates language servers from code editors (language clients). By making language servers independent processes dedicated to language understanding, the LSP enables any editor to utilize a standard language server. Which means that a single standard language server can be used by all editors.</p></li><li><p>This interoperability is achieved through a defined set of standard messages and procedures that govern communication between language servers and editors. The LSP defines the format of the messages sent using JSON-RPC between the development tool and the language server.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F5Qc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F5Qc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 424w, https://substackcdn.com/image/fetch/$s_!F5Qc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 848w, https://substackcdn.com/image/fetch/$s_!F5Qc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 1272w, https://substackcdn.com/image/fetch/$s_!F5Qc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F5Qc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png" width="1277" height="483" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:483,&quot;width&quot;:1277,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:80214,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!F5Qc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 424w, https://substackcdn.com/image/fetch/$s_!F5Qc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 848w, https://substackcdn.com/image/fetch/$s_!F5Qc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 1272w, https://substackcdn.com/image/fetch/$s_!F5Qc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F982fc5c0-8013-45c4-863d-d67a3532292a_1277x483.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Modern-day code editors using LSP</figcaption></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h2>Language Server Features</h2><p>The list of features may vary for each individual language server, but usually they provide the following functionalities:</p><ul><li><p>Auto-completion</p></li><li><p>Go to definition/declaration</p></li><li><p>Find references</p></li><li><p>Code formatting</p></li><li><p>Diagnostics</p></li><li><p>Documentation</p></li><li><p>etc.</p></li></ul><p>For example, <a href="https://github.com/golang/tools/blob/master/gopls/doc/features/README.md">here</a> you can see the list of editor features that <strong>gopls </strong>(Go Language Server) provides.</p><p>And <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#languageFeatures">here</a> you can see the full LSP specification for available features.</p><h2>How does LSP work?</h2><p>The Language Server Protocol is built upon <a href="https://www.jsonrpc.org/">JSON-RPC</a>. It specifically uses JSON RPC 2.0. You can think of JSON-RPC as a remote procedure call protocol that uses JSON for data encoding.</p><p>In a nutshell it works like this. First, the editor establishes a connection with the language server, then as the developer types code, the editor sends incremental changes to the language server. It then sends back insights like: code completion suggestions, diagnostics.</p><p>Let&#8217;s see one real example for auto-completion. The request from Language Client (editor) for this case will be:</p><pre><code>{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/completion",
  "params": {
    "textDocument": {
      "uri": "file:///home/alex/code/test/main.go"
    },
    "position": {
      "line": 35,
      "character": 21
    }
  }
}</code></pre><p>As you can see it sends the information about current cursor position and the buffer file. Let's break it down:</p><ul><li><p><strong>ID</strong>: The client sets this field to identify the request uniquely. Once the request is processed, it will return a response with the same request ID so that the client can match which response is for what request.</p></li><li><p><strong>Method</strong>: A string containing the name of the method to be invoked.</p></li><li><p><strong>Params</strong>: The parameters to be passed to the method. This can be structured as an array or an object.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dGep!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dGep!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 424w, https://substackcdn.com/image/fetch/$s_!dGep!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 848w, https://substackcdn.com/image/fetch/$s_!dGep!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 1272w, https://substackcdn.com/image/fetch/$s_!dGep!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dGep!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png" width="1456" height="560" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:560,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:46402,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://packagemain.tech/i/153771446?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dGep!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 424w, https://substackcdn.com/image/fetch/$s_!dGep!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 848w, https://substackcdn.com/image/fetch/$s_!dGep!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 1272w, https://substackcdn.com/image/fetch/$s_!dGep!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd12d9a7-8644-4961-ba94-ef7cb15462ce_1554x598.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Language server can access this file, analyze it and respond with suggestions:</p><pre><code>{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "isIncomplete": false,
    "items": [
      {
        "label": "Println",
        "kind": 3,
        "insertText": "Println(${1:format}, ${2:a ...interface{}})$0",
        "insertTextFormat": 2,
        "detail": "func Println(a ...interface{}) (n int, err error)",
        "documentation": "Println formats ..."
      },
      // ... other items
    ]
  }
}</code></pre><h2>Language Server for Go</h2><p>The most popular and commonly used language server for Go is <a href="https://github.com/golang/tools/tree/master/gopls">gopls</a>. It is used by many editors, for example by <a href="https://github.com/golang/vscode-go">Visual Studio Code Go extension</a>. Previously, there was another popular Language Server for Go by the Sourcegraph team called <a href="https://github.com/sourcegraph/go-langserver">go-langserver</a>, but this is no longer under active maintenance.</p><p>Many editors install gopls Language Server automatically if it&#8217;s not present on the host machine, but you can install it manually as well:</p><pre><code>go install golang.org/x/tools/gopls@latest</code></pre><p>gopls also provides an experimental CLI interface:</p><pre><code>$ gopls references ./test.go:35:8

/Users/alex/code/test/test.go:55:37-51</code></pre><h2>Editors</h2><p>Usually, you do not have to start the Language Server yourself as your editor is doing it for you. It also only starts the language server if a specific file has been opened. You can confirm it by checking the processes your editor has created, for example:</p><pre><code>ps aux| grep gopls</code></pre><h2>Conclusion</h2><p>Thanks to the Language Server Protocol, advanced coding capabilities are becoming universally accessible across various programming languages and coding environments.</p><p>It&#8217;s good to know how your code editors work, therefore it&#8217;s beneficial to have an understanding of this widely used technology called LSP.</p><p>LSP is a win for both language providers and tooling vendors!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h2>Resources</h2><ul><li><p><a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a></p></li><li><p><a href="https://github.com/golang/tools/tree/master/gopls">gopls</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Integration Tests with GitHub Service Containers]]></title><description><![CDATA[A tutorial on running integrations tests with dependencies in Github Actions workflows.]]></description><link>https://packagemain.tech/p/integration-tests-with-github-service</link><guid isPermaLink="false">https://packagemain.tech/p/integration-tests-with-github-service</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Fri, 27 Dec 2024 13:16:22 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7c7f1ac7-fb04-4c85-b217-c3ffd594ccc4_1344x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Not so long ago we published an <strong><a href="https://packagemain.tech/p/integration-tests-using-testcontainers">article</a></strong> about using <strong><a href="https://testcontainers.com/">Testcontainers</a></strong> for emulating external dependencies such as a database and cache for the purpose of backend integration tests. The article also explains the different ways of running the integration tests, environment scaffolding and their pros and cons.</p><p>In this post we want to show another alternative in case you use GitHub Actions as your CI platform, which became the most popular CI/CD solution at the moment.</p><p>It&#8217;s called <strong><a href="https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/about-service-containers">Service Containers</a></strong>, and we realised that, unfortunately, not so many developers are aware of it.</p><p>In this hands-on tutorial we want to demonstrate how to create a GitHub Actions workflow for integration tests with external dependencies (MongoDB and Redis), using the <a href="https://github.com/plutov/packagemain/tree/master/testcontainers-demo">demo Go application</a> we wrote previously, as well as review the pros and cons of GitHub Service Containers.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h2>What are Service Containers?</h2><p>Service Containers are Docker containers that offer a simple and portable way to host dependencies like databases (MongoDB in our example), web services, or caching systems (Redis in our example) that your application needs within a workflow. This article focuses on integration tests, however there are many other possible applications. Service containers can also be used to run supporting tools required by your workflow, such as code analysis tools, linters, or security scanners.</p><h2>Why not Docker Compose?</h2><p>Sounds similar to <strong>services</strong> in Docker Compose, right? Because it is.</p><p>However, while you could technically <a href="https://github.com/marketplace/actions/docker-compose-action">use Docker Compose</a> within a GitHub Actions workflow by installing Docker Compose and running the <strong>docker-compose up</strong>, service containers provide a more integrated and streamlined approach specifically designed for the GitHub Actions environment.</p><p>Also, while they are similar, they solve different purpose.</p><ul><li><p>Docker Compose is good when you need to manage a multi-container application on your local machine or a single server. best suited for long-living environments.</p></li><li><p>Service Containers are ephemeral and exist only for the duration of a workflow run, and defined directly within your GitHub Actions workflow file.</p></li></ul><p>Also, the feature set of service containers (at least as of now) is more limited compared to Docker Compose, so be ready to discover some potential bottlenecks, we will cover some of them at the end of this article.</p><h2>Job runtime</h2><p>You can run GitHub jobs directly on a runner machine or in a Docker container (by specifying <strong>container</strong> property). The second option simplifies the access to your services by using labels you defined in the <strong>services</strong> section.</p><p>Run directly on a runner machine.</p><blockquote><p><em><strong>.github/workflows/test.yaml</strong></em></p></blockquote><pre><code>jobs:
  integration-tests:
    runs-on: ubuntu-24.04

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017:27017

    steps:
      - run: |
          echo "addr 127.0.0.1:27017"</code></pre><p>Or run in a container (<a href="https://images.chainguard.dev/directory/image/go/overview">Chainguard Go Image</a> in our case):</p><pre><code><code>jobs:
  integration-tests:
    runs-on: ubuntu-24.04
    container: cgr.dev/chainguard/go:latest

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017:27017

    steps:
      - run: |
          echo "addr mongo:27017"</code></code></pre><p>You can also omit the host port, so the container port will be randomly assigned to a free port on the host. You can then access the port using the variable.</p><pre><code><code>jobs:
  integration-tests:
    runs-on: ubuntu-24.04
    container: cgr.dev/chainguard/go:1.23

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017/tcp

    steps:
      - run: |
          echo "addr mongo:${{ job.services.mongo.ports['27017'] }}"</code></code></pre><h2>Readiness Healthcheck</h2><p>Prior to running the job steps that connect to our provisioned containers we sometimes need to make sure that the services are ready. It&#8217;s possible to do so by specifying <a href="https://docs.docker.com/reference/cli/docker/container/create/#options">docker create options</a> such as <strong>health-cmd</strong>.</p><p>This is very important, otherwise the services may not be ready when we start accessing them in the <strong>steps</strong> section.</p><p>In a case of MongoDB and Redis these will be:</p><pre><code>    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017/27017
        options: &gt;-
          --health-cmd "echo 'db.runCommand("ping").ok' | mongosh mongodb://localhost:27017/test --quiet"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: &gt;-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10</code></pre><h2>Private Container Registries</h2><p>In our example we use public images from Dockerhub, however it&#8217;s possible to use private images as well and pass the credentials:</p><pre><code>services:
  private_service:
    image: ghcr.io/org/service_repo
    credentials:
      username: ${{ secrets.registry_username }}
      password: ${{ secrets.registry_token }}</code></pre><h2>Sharing data between services</h2><p>You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host. However, it&#8217;s not directly possible to mount the source code as a container volume. <a href="https://github.com/orgs/community/discussions/42127">Open discussion</a></p><pre><code>volumes:
  - /src/dir:/dst/dir</code></pre><h2>Golang Integration Tests</h2><p>Now as we can provision our external dependencies, let&#8217;s have a look at how to run our integration tests in Go. We will do it in the <strong>steps</strong> section of our workflow file.</p><p>We will run our tests in a container which uses <a href="https://images.chainguard.dev/directory/image/go/overview">Chainguard Go image</a>, which means we don&#8217;t have to install/setup Go. In case you want to run your tests directly on a runner machine, you need to use <a href="https://github.com/actions/setup-go">setup-go</a> Action.</p><p>You can find the full source code with tests and this workflow <a href="https://github.com/plutov/service-containers">here</a>.</p><blockquote><p><em><strong>.github/workflows/integration-tests.yaml</strong></em></p></blockquote><pre><code>name: "integration-tests"

on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  integration-tests:
    runs-on: ubuntu-24.04
    container: cgr.dev/chainguard/go:latest

    env:
      MONGO_URI: mongodb://mongo:27017
      REDIS_URI: redis://redis:6379

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017:27017
        options: &gt;-
          --health-cmd "echo 'db.runCommand("ping").ok' | mongosh mongodb://localhost:27017/test --quiet"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: &gt;-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

    steps:
      - name: Check out repository code
        uses: actions/checkout@v4

      - name: Download dependencies
        run: go mod download

      - name: Run Integration Tests
        run: go test -tags=integration -timeout=120s -v ./...</code></pre><p>To summarize:</p><ol><li><p>We run our job in a container with Go (<strong>container</strong>)</p></li><li><p>We spin up 2 services: MongoDB and Redis (<strong>services</strong>)</p></li><li><p>We configure healthchecks to make sure our services are &#8220;Healthy&#8221; when we run the tests (<strong>options</strong>)</p></li><li><p>Standard code checkout</p></li><li><p>Run the Go tests</p></li></ol><p>Once the Action is completed (it took <strong>~1 min</strong> for this example), all the services will be stopped and orphaned so we don&#8217;t need to worry about that.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_Y9r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_Y9r!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 424w, https://substackcdn.com/image/fetch/$s_!_Y9r!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 848w, https://substackcdn.com/image/fetch/$s_!_Y9r!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 1272w, https://substackcdn.com/image/fetch/$s_!_Y9r!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_Y9r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png" width="480" height="409" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:409,&quot;width&quot;:480,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39930,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_Y9r!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 424w, https://substackcdn.com/image/fetch/$s_!_Y9r!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 848w, https://substackcdn.com/image/fetch/$s_!_Y9r!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 1272w, https://substackcdn.com/image/fetch/$s_!_Y9r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af2df88-ec95-41e9-9bab-bb22ea2deb10_480x409.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Personal Experience &amp; Limitations</h2><p>We&#8217;ve been using service containers for running backend integration tests at <a href="https://www.binarly.io/">BINARLY</a> for some time, and they work great. However, the initial workflow creation took some time and we encountered the following bottlenecks:</p><ul><li><p>It&#8217;s not possible to override or run custom command in an action service container (as you would do in Docker Compose using <strong>command</strong> property). <a href="https://github.com/actions/runner/pull/1152">Open pull request</a></p></li><li><p>It&#8217;s not directly possible to mount the source code as a container volume. <a href="https://github.com/orgs/community/discussions/42127">Open discussion</a></p></li></ul><h2>Conclusion</h2><p>GitHub service containers is a great option to scaffold the ephemeral testing environment by configuring it directly in your GitHub workflow. With configuration somehow similar to Docker Compose it&#8217;s easy to run any containerised application and to communication with it in your pipeline, making sure that GitHub runners take care of shutting everything down upon completion.</p><p>If you use Github Actions, this approach works extremely well as it is specifically designed for the GitHub Actions environment.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h2>Resources</h2><ul><li><p><a href="https://github.com/plutov/service-containers">Source Code</a></p></li><li><p><a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices">GitHub Documentation</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Fuzz Testing Go HTTP Services]]></title><description><![CDATA[As a developer, you can't envision all of the possible inputs your programs or functions could receive.]]></description><link>https://packagemain.tech/p/fuzzing-http-services-golang</link><guid isPermaLink="false">https://packagemain.tech/p/fuzzing-http-services-golang</guid><dc:creator><![CDATA[Alex Pliutau]]></dc:creator><pubDate>Mon, 28 Oct 2024 11:09:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9e4d2060-1233-4365-bbce-02be04aa3f45_2912x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a developer, you can't envision all of the possible inputs your programs or functions could receive. Even though you can define the major edge cases, you still can't predict how your program will behave in the case of some weird unexpected input. In other words, you can only find bugs you <em>expect</em> to find.</p><p>That's where fuzz testing or fuzzing comes to the rescue.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h2>What is Fuzz Testing</h2><p>Fuzzing is an automated software testing technique that involves inputting a large amount of valid, nearly-valid or invalid random data into a computer program and observing its behavior and output. So the goal of fuzzing is to reveal bugs, crashes and security vulnerabilities in source code you might not find through traditional testing methods.</p><p>The <a href="https://youtu.be/w8STTZWdG9Y">Fuzz Testing in Go</a> video which I made a few years ago shows a very simple example of the Go code that may work well unless you provide a certain input:</p><pre><code><code>func Equal(a []byte, b []byte) bool {
  for i := range a {
    // can panic with runtime error: index out of range.
    if a[i] != b[i] {
      return false
    }
  }

  return true
}</code></code></pre><p>Fuzzing technique would easily spot this bug by bombarding this function with various inputs.</p><p>It's a good practice to integrate fuzzing into your team&#8217;s software development lifecycle (SDLC) as well. For example, Microsoft uses fuzzing as <a href="https://learn.microsoft.com/en-us/compliance/assurance/assurance-microsoft-security-development-lifecycle">one of the stages in its SDLC</a>, to find potential bugs and vulnerabilities.</p><h2>Fuzz Testing in Go</h2><p>There are many fuzzing tools that have been available for a while such as <a href="https://github.com/google/oss-fuzz">oss-fuzz</a> for example, but since Go 1.18 fuzzing was added to Go's standard library and is part of the regular testing package since it is a kind of test. It can also be used together with the other testing primitives which is nice.</p><p>The steps to create a fuzz test in Go are the following:</p><ol><li><p>In a <strong>_test.go</strong> file create a function that starts with <strong>Fuzz</strong> which accepts <strong>*testing.F</strong></p></li><li><p>Add corpus seeds using <strong>f.Add()</strong> to allow fuzzer to generate the data based on it.</p></li><li><p>Call fuzz target using <strong>f.Fuzz()</strong> by passing fuzzing arguments which our target function accepts.</p></li><li><p>Start the fuzzer using regular&nbsp;<strong>go test</strong>&nbsp;command, but with the&nbsp;<strong>&#8211;fuzz=Fuzz</strong>&nbsp;flag</p></li></ol><p>Note, the fuzzing arguments can only be the following types:</p><ul><li><p>string,&nbsp;byte, []byte</p></li><li><p>int,&nbsp;int8,&nbsp;int16,&nbsp;int32/rune,&nbsp;int64</p></li><li><p>uint,&nbsp;uint8,&nbsp;uint16,&nbsp;uint32,&nbsp;uint64</p></li><li><p>float32,&nbsp;float64</p></li><li><p>bool</p></li></ul><p>A simple fuzz test for the <strong>Equal</strong> function above may look like this:</p><pre><code>// Fuzz test
func FuzzEqual(f *testing.F) {

  // Seed corpus addition
  f.Add([]byte{'f', 'u', 'z', 'z'}, []byte{'t', 'e', 's', 't'})

  // Fuzz target with fuzzing arguments
  f.Fuzz(func(t *testing.T, a []byte, b []byte) {
    // Call our target function and pass fuzzing arguments
    Equal(a, b)
  })
}</code></pre><p>By default, fuzz tests run forever,&nbsp;so you either need to specify the time limit or wait for fuzz tests to fail. You can specify which tests to run using <strong>--fuzz</strong> argument.</p><pre><code><code>go test --fuzz=Fuzz -fuzztime=10s</code></code></pre><p>If there are any errors during the execution, the output should look similar to this:</p><pre><code><code>go test --fuzz=Fuzz -fuzztime=30s
--- FAIL: FuzzEqual (0.02s)
    --- FAIL: FuzzEqual (0.00s)
        testing.go:1591: panic: runtime error: index out of range
    Failing input written to testdata/fuzz/FuzzEqual/84ed65595ad05a58
    To re-run:
    go test -run=FuzzEqual/84ed65595ad05a58</code></code></pre><p>Notice that the input for which fuzz test has failed are written into a file in <code>testdata</code> folder and can be re-played by using that input identifier.</p><pre><code><code>go test -run=FuzzEqual/84ed65595ad05a58</code></code></pre><p>The <strong>testdata</strong> folder can be checked into the repository and be used for regular tests, because fuzz tests can also act as regular tests when executed without <strong>--fuzz</strong> flag.</p><h2>Fuzzing HTTP Services</h2><p>It's also possible to fuzz test the http services by writing a test for your <strong>HandlerFunc</strong> and using the <strong>httptest</strong> package. Which can be very useful to test the whole HTTP service, not only the underlying functions.</p><p>Let's now introduce a more real example such as an HTTP Handler that accepts some user input in the request body and then write a fuzz test for it.</p><p>Our handler accepts JSON request with <code>limit</code> and <code>offset</code> fields to paginate some static mocked data. Let's define the types first.</p><pre><code><code>type Request struct {
  Limit  int `json:"limit"`
  Offset int `json:"offset"`
}

type Response struct {
  Results    []int `json:"items"`
  PagesCount int   `json:"pagesCount"`
}</code></code></pre><p>Our handler function then parses the JSON, paginates the static slice and returns a new JSON in response.</p><pre><code><code>func ProcessRequest(w http.ResponseWriter, r *http.Request) {
  var req Request

  // Decode JSON request
  if err := json.NewDecoder(r.Body).Decode(&amp;req); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  // Apply offset and limit to some static data
  all := make([]int, 1000)
  start := req.Offset
  end := req.Offset + req.Limit
  res := Response{
    Results:    all[start:end],
    PagesCount: len(all) / req.Limit,
  }

  // Send JSON response
  if err := json.NewEncoder(w).Encode(res); err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }

  w.WriteHeader(http.StatusOK)
}</code></code></pre><p>As you may have already noticed, this function doesn't handle slice operations quite well and can easily <code>panic</code>. Also it can panic if it tries to divide by 0. It's great if we can spot it during the development or using only unit tests, but sometimes not everything is visible to our eye, and our handler may pass the input to other functions and so forth.</p><p>Following our <strong>FuzzEqual</strong> example above, let's implement a fuzz test for the <code>ProcessRequest</code> handler. The first thing we need to do is to provide the sample inputs for the fuzzer. This is the data that the fuzzer will use and modify into new inputs that are tried. We can craft some sample JSON request and use <strong>f.Add()</strong> with <strong>[]byte</strong> type.</p><pre><code><code>func FuzzProcessRequest(f *testing.F) {
  // Create sample inputs for the fuzzer
  testRequests := []Request{
    {Limit: -10, Offset: -10},
    {Limit: 0, Offset: 0},
    {Limit: 100, Offset: 100},
    {Limit: 200, Offset: 200},
  }

  // Add to the seed corpus
  for _, r := range testRequests {
    if data, err := json.Marshal(r); err == nil {
      f.Add(data)
    }
  }

  // ...
}</code></code></pre><p>After that we can use <strong>httptest</strong> package to create a test HTTP server and make requests to it.</p><p>Note: Since our fuzzer can generate invalid non-JSON requests, it's better just to skip them and ignore with <strong>t.Skip()</strong>. We can also skip <strong>BadRequest</strong> errors.</p><pre><code><code>func FuzzProcessRequest(f *testing.F) {
  // ...

  // Create a test server
  srv := httptest.NewServer(http.HandlerFunc(ProcessRequest))
  defer srv.Close()

  // Fuzz target with a single []byte argument
  f.Fuzz(func(t *testing.T, data []byte) {
    var req Request
    if err := json.Unmarshal(data, &amp;req); err != nil {
      // Skip invalid JSON requests that may be generated during fuzz
      t.Skip("invalid json")
    }

    // Pass data to the server
    resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data))
    if err != nil {
      t.Fatalf("unable to call server: %v, data: %s", err, string(data))
    }

    defer resp.Body.Close()

    // Skip BadRequest errors
    if resp.StatusCode == http.StatusBadRequest {
      t.Skip("invalid json")
    }

    // Check status code
    if resp.StatusCode != http.StatusOK {
      t.Fatalf("non-200 status code %d", resp.StatusCode)
    }
  })
}</code></code></pre><p>Our fuzz target has a single argument with a type <strong>[]byte</strong> that contains the full JSON request, however you can change it to have multiple arguments.</p><p>Everything is ready now to run our fuzz tests. When fuzzing http servers, you may need to adjust the amount of parallel workers, otherwise the load may overwhelm the test server. You can do that by setting <strong>-parallel=1 </strong>flag.</p><pre><code><code>go test --fuzz=Fuzz -fuzztime=10s -parallel=1</code></code></pre><p>And as expected we will see the following errors uncovered.</p><pre><code><code>go test --fuzz=Fuzz -fuzztime=30s
--- FAIL: FuzzProcessRequest (0.02s)
    --- FAIL: FuzzProcessRequest (0.00s)
        runtime error: integer divide by zero
        runtime error: slice bounds out of range</code></code></pre><p>We can also see the fuzz inputs in the <code>testdata</code> folder to see which JSON contributed to this failure. Here is a sample content of the file:</p><pre><code><code>go test fuzz v1
[]byte("{"limit":0,"offset":0}")</code></code></pre><p>To fix that issue we can introduce input validation and default settings:</p><pre><code><code>if req.Limit &lt;= 0 {
  req.Limit = 1
}

if req.Offset &lt; 0 {
  req.Offset = 0
}

if req.Offset &gt; len(all) {
  start = len(all) - 1
}

if end &gt; len(all) {
  end = len(all)
}</code></code></pre><p>With this change the fuzz tests will run for 10 seconds and exit without an error.</p><h2>Conclusion</h2><p>Writing fuzz tests for your HTTP services or any other methods is a great way to detect hard-to-find bugs. Fuzzers can detect hard-to-spot bugs that happen for only some weird unexpected input.</p><p>It's amazing to see that fuzzing is a part of built-in testing library, making it easy to combine with regular tests. Note: prior to Go 1.18 developers used <a href="https://github.com/dvyukov/go-fuzz">go-fuzz</a>, which is a great tool for fuzzing as well.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packagemain.tech/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packagemain.tech/subscribe?"><span>Subscribe now</span></a></p><h2>Resources</h2><ul><li><p><a href="https://github.com/plutov/packagemain/tree/master/fuzz-testing-http-services">Source Code</a></p></li><li><p><a href="https://youtu.be/w8STTZWdG9Y">Fuzz Testing in Go</a></p></li><li><p><a href="https://go.dev/doc/security/fuzz/">Go Fuzzing</a></p></li></ul>]]></content:encoded></item></channel></rss>