<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: boom</title>
    <description>The latest articles on DEV Community by boom (@boomboompower).</description>
    <link>https://dev.to/boomboompower</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1395548%2F5d5420d3-6939-45a7-bcd8-a9b232430505.jpeg</url>
      <title>DEV Community: boom</title>
      <link>https://dev.to/boomboompower</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boomboompower"/>
    <language>en</language>
    <item>
      <title>Naust JMAP: a runtime for Go, not just a mail server</title>
      <dc:creator>boom</dc:creator>
      <pubDate>Tue, 04 Aug 2026 02:20:12 +0000</pubDate>
      <link>https://dev.to/boomboompower/naust-jmap-a-runtime-for-go-not-just-a-mail-server-23n8</link>
      <guid>https://dev.to/boomboompower/naust-jmap-a-runtime-for-go-not-just-a-mail-server-23n8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A cut-down version of the &lt;a href="https://boomy.wtf/projects/naust-jmap" rel="noopener noreferrer"&gt;full write-up&lt;/a&gt;. As of writing Naust JMAP is still in pre-release and is subject to breaking changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What if you could build a complete JMAP server without implementing the protocol yourself? Naust JMAP owns protocol correctness, your datatypes own what the objects mean, and your backend owns where they live.&lt;/p&gt;

&lt;p&gt;Naust JMAP (&lt;a href="https://github.com/naust-mail/naust-jmap" rel="noopener noreferrer"&gt;repo&lt;/a&gt;) is a Go framework for building JMAP servers. It provides the protocol runtime required by RFC 8620 (Core), RFC 8621 (Mail), and related extensions, while leaving storage, authentication, search, delivery, and application-specific data models to the user.&lt;/p&gt;

&lt;p&gt;The distinction that matters is runtime rather than server. Most JMAP implementations are complete mail servers, where the protocol sits on top of storage, delivery and authentication decisions that were made for you. Here those decisions stay with whoever embeds it. When I started there was also no server-side JMAP engine in Go at all, only client libraries.&lt;/p&gt;

&lt;p&gt;JMAP was written for mail and calendaring, but the core specification never mentions email. It describes typed objects, method calls against them, and a synchronisation model. Mail is the first datatype built on that, not the point of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The core runtime manages JMAP concepts such as sessions, method dispatch, standard method semantics, state strings, change tracking, blobs, and push notifications. Applications provide implementations through small interfaces: authentication, storage, blob persistence, search, notifications, and custom datatypes.&lt;/p&gt;

&lt;p&gt;A datatype starts as a declaration rather than a set of handlers. Describe a type once and the runtime derives its &lt;code&gt;/get&lt;/code&gt;, &lt;code&gt;/changes&lt;/code&gt;, &lt;code&gt;/set&lt;/code&gt;, &lt;code&gt;/copy&lt;/code&gt;, &lt;code&gt;/query&lt;/code&gt; and &lt;code&gt;/queryChanges&lt;/code&gt; methods. No method code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Define a type with two properties, one indexed and one with a default value&lt;/span&gt;
&lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"Todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Capability&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"urn:example:todo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KindString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Indexed&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"done"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KindBool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Indexed&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`false`&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Register it with the runtime, which derives the standard methods&lt;/span&gt;
&lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewProcessor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;core&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultCoreCapabilities&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterStandardType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mail plugin declares a Mailbox the same way, with a name, a role and four counters, marking each property as client-writable, server-owned, or worth indexing.&lt;/p&gt;

&lt;p&gt;Because the runtime generates those methods for any type it is given, it will also generate ones the specification never defined. Letting a type restrict which of them it wants caught a bug I had already shipped: RFC 8621 defines copy for Email alone, and Mailbox and Thread were both advertising it to clients.&lt;/p&gt;

&lt;p&gt;Storage sits behind ordered key-value operations with an atomic batch, which keeps a new backend small. The two that ship are SQLite and Postgres, so this is not a storage engine: the indexes, change tracking and query planner are bookkeeping built once above someone else's engine, and they serve every datatype.&lt;/p&gt;

&lt;p&gt;Change tracking is the part that has to be exactly right, so a change log entry is written in the same atomic batch as the mutation that caused it, rather than derived from it afterwards; a log that subscribes to writes loses data the first time the subscriber falls over. Concurrent writers are handled by a single-writer lease per account, claimed with a compare-and-swap rather than a database lock, whose token is also the fence checked at commit. A holder that stalls and comes back has its writes rejected, so consistency comes from the lease and the batch rather than from any backend's transaction isolation, and never from a timeout being tuned correctly.&lt;/p&gt;

&lt;p&gt;The claim that correctness can live in the framework only holds if something checks it. Every backend runs one shared contract suite, and the object store is checked against a naive in-memory model with property-based tests. The WebSocket codec is hand-rolled, so it runs against the Autobahn suite, which is how I found out that validating UTF-8 per message is not the same as validating it incrementally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;p&gt;Building this as a library rather than a server is only possible because of what JMAP is. IMAP is a long-lived session, so an implementation owns connection state and effectively has to be the process. JMAP is stateless request and response, so there is nothing to own between calls, an embedder can hold as many instances as they like, and a restart is invisible to clients rather than something they recover from.&lt;/p&gt;

&lt;p&gt;The core module depends on nothing but the Go standard library, enforced by a test rather than a convention. This is a supply chain decision. Go resolves dependencies per module, so anything carrying one becomes its own module, otherwise it shows up in the audit output of everyone importing the runtime whether they use that part or not. A vulnerability in the charset library still forced a Go version floor rearrangement across three modules. Quarantine decides which module has the problem, not whether you have one.&lt;/p&gt;

&lt;p&gt;Most of the design work was deciding what not to build. The core has no opinion about anything the RFCs are silent on. There is no search index, only a two-method interface with a substring scan behind it, since a scan answers a text filter correctly and everything past that fragments into ranking, tokenisation, stemming and incremental reindexing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mail.&lt;/strong&gt; Every mail feature would be easier with a table, an index or a state string that only mail is allowed to have.&lt;/p&gt;

&lt;p&gt;Threading is server-defined, so there is no correct answer to check against. Messages join on a shared message-id and an equal base subject, with no merge, so a late linking message joins the first matching thread and threads that have split stay split. Merging would mean destroying and reinserting, because thread ids are immutable.&lt;/p&gt;

&lt;p&gt;The first threading implementation was O(N squared). A 1600-message thread took 140 seconds to ingest, because each arrival loaded every candidate record to compare subjects. It now hashes the message-id and base subject together into a single composite index, so an arrival does one lookup per referenced id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling.&lt;/strong&gt; Most of what I initially read off the graphs was wrong. My harness took the maximum rather than the sum of per-process memory, so forking servers looked about six times leaner than they were, and it reused a single message for every delivery in a run, which turned a throughput column I had been reading for weeks into a measure of duplicate-message contention. One of the gaps I had been trying to close turned out to be a competitor storing one copy of forty identical messages rather than storing them faster.&lt;/p&gt;

&lt;p&gt;What survived the corrections was a storage model difference rather than a code problem. Dovecot and Cyrus spool messages to files, so the bytes sit in page cache the kernel can reclaim, whereas a transactional key-value store generally has to materialise a value in the process before it can commit it. That is the price of a blob committing in the same transaction as the objects referencing it. A file-backed store ships too and measures better on both throughput and memory, and what it gives up is exactly that property, which is not something you can add back afterwards.&lt;/p&gt;

&lt;p&gt;Within that constraint there was still a factor of four available, found by changing how blobs are written, with the same binary, the same parser and the same messages. The chunked store only streamed blobs larger than a single piece, so ordinary mail was buffered whole, and it reserved a full piece per concurrent writer regardless of message size. Pieces now start at 256 KiB and double up to the cap. That was only legal because the manifest records a piece count and not a piece size, so nothing downstream can observe where the boundaries fell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;p&gt;The hardest part of implementing a protocol is not parsing requests, but defining the boundary between what the protocol owns and what the application owns.&lt;/p&gt;

&lt;p&gt;Conformance and performance are separate claims. A substring scan answers a text query correctly, just slowly, so a real index buys speed and closes no compliance gap. That removed an entire milestone from the roadmap.&lt;/p&gt;

&lt;p&gt;The rules only held once they were executable. Two design drifts happened by reasoning forward from a plausible premise past a rule I had already written down, and both were caught by review rather than by anything automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;S/MIME verification and quotas as further RFC 8621-family modules, streaming writes on the backend interface if the atomicity survives it, and more breaking changes pre-1.0 :))&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://boomy.wtf/projects/naust-jmap" rel="noopener noreferrer"&gt;full write-up&lt;/a&gt; has the parts I cut here: the transport boundary, the web push crypto, and what each of them cost. Quickstart, guides and the method-by-method RFC matrices live in the &lt;a href="https://naust.email/naust-jmap" rel="noopener noreferrer"&gt;docs&lt;/a&gt;, and the code is &lt;a href="https://github.com/naust-mail/naust-jmap" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Chafa Frontend</title>
      <dc:creator>boom</dc:creator>
      <pubDate>Wed, 11 Jun 2025 19:49:39 +0000</pubDate>
      <link>https://dev.to/boomboompower/chafa-frontend-4c1</link>
      <guid>https://dev.to/boomboompower/chafa-frontend-4c1</guid>
      <description>&lt;h2&gt;
  
  
  I Built a Web UI for Chafa So You Don’t Have to Touch the Terminal (Unless You Want To)
&lt;/h2&gt;

&lt;p&gt;A few months ago, I came across &lt;a href="https://hpjansson.org/chafa/" rel="noopener noreferrer"&gt;Chafa&lt;/a&gt; — a command-line tool that turns images into ANSI/ASCII art. If you’ve never tried it, it’s kind of brilliant. You point it at a PNG or JPEG, and it spits out a wall of colored text that looks eerily close to the original image — all using braille, Unicode blocks, or ASCII characters.&lt;/p&gt;

&lt;p&gt;Naturally, I did what any curious dev would do:&lt;br&gt;&lt;br&gt;
I fed it a few pictures, sat back, and watched my terminal become an art gallery.&lt;/p&gt;

&lt;p&gt;But then I had a thought:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;What if you didn’t have to install anything? What if you could just drop an image into your browser and get the same result — instantly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s how this started.&lt;/p&gt;




&lt;h3&gt;
  
  
  Introducing: Chafa Frontend
&lt;/h3&gt;

&lt;p&gt;This is a minimal web app that wraps the Chafa CLI in a Flask server and adds a browser-based UI on top.&lt;br&gt;&lt;br&gt;
No terminal. No CLI flags. Just upload → configure → render.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/boomboompower/chafa-frontend" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I didn’t write the magic — Chafa does all the heavy lifting in the project. Ironally, most of the code in this repo is for getting material design working, or even just validation. All-in-all, this project just tries to make Chafa more accessible.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Build a Web UI for a CLI Tool?
&lt;/h3&gt;

&lt;p&gt;Honestly? Because I wanted to use Chafa on a phone.&lt;br&gt;&lt;br&gt;
And I wanted to share ASCII with my friends who have never opened a terminal in their lives.&lt;/p&gt;

&lt;p&gt;Also: I think good CLI tools deserve good UIs. Not because the CLI is lacking, but because &lt;em&gt;a tool can be powerful and approachable at the same time&lt;/em&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;The frontend is a simple single-page app using Material Design 3 components — just enough UI to pick a file, select some Chafa options, and display the result. No frameworks, no build steps — just HTML/CSS/JS.&lt;/p&gt;

&lt;p&gt;The backend is a lightweight Flask server. When you upload an image, here’s what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The image and selected options are sent to the backend.&lt;/li&gt;
&lt;li&gt;The backend validates the inputs, saves the image temporarily.&lt;/li&gt;
&lt;li&gt;It invokes &lt;code&gt;chafa&lt;/code&gt; with the requested options.&lt;/li&gt;
&lt;li&gt;The ANSI output is returned to the frontend and rendered in the browser using &lt;a href="https://github.com/drudru/ansi_up" rel="noopener noreferrer"&gt;&lt;code&gt;ansi_up&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why Material Design 3?
&lt;/h2&gt;

&lt;p&gt;No big reason — I just liked the idea of giving a CLI tool a polished, mobile-friendly interface. Material Web Components made it easy to prototype quickly and still look decent across devices. Mainly, I wanted to play around with some of the latest standards for material design - as most of my current projects have been using Material Design 2 components of some kind.&lt;/p&gt;




&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Building for “non-technical users” forces you to think carefully about defaults and feedback.&lt;/li&gt;
&lt;li&gt;Even tools designed for terminals can benefit from a little UX thinking.&lt;/li&gt;
&lt;li&gt;Chafa is extremely fast. I never had to optimize anything — it just works.&lt;/li&gt;
&lt;li&gt;Material Design 3 (at least when working purely in DOM), feels like it has a long way to go. I read through the documentation looking for formatting, and was puzzled when the components wouldn't style properly. Once I read through the source code there were frequent mismatches which was a shame.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;This isn’t a reinvention of image-to-ASCII conversion — that credit goes entirely to Chafa and Hans Petter Jansson. This is just a small wrapper to make it more usable in a browser context.&lt;/p&gt;

&lt;p&gt;If you’re into terminal aesthetics, retro graphics, or building useful frontends around powerful backends — check it out! If you have any suggestions or extensions, feel free to let me know or fork it 🙂&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/boomboompower/chafa-frontend" rel="noopener noreferrer"&gt;Check it out&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
