drshapeless


Rewriting my htmx service with sqlite and pure css

Tags: sqlite3 | htmx | go

Create: 2026-01-09, Update: 2026-01-09

Background

A while ago, I was added to the Jai beta. Since then, most of my time was spent on tinkering with Jai, creating different bindings and making games.

Just recently, I discovered that htmx is having a new version, htmx 4. I thought that was a joke, but it turns out to be a real thing. It brings a lot of new features like built-in morphing. The most attracting feature is the hx-partial tag. I do a lot of oob swap.

And I decide to rewrite my service to htmx 4. Along side with that, I want to make some fundamental changes to the stacks I choose.

From postgres to sqlite

Postgres was my first database choice to do any services in the past. It was fine, fast, easy to maintain. But it is too overkill. For the self hosted services I wrote, the database is always located in the same machine, insertions and queries number is extremely low, because I am the only user.

Sqlite is enough for my use case, and does not use a server client model. Backing up the database is as simple as copying a file.

But sqlite library is another rabbit hole in Go. Since Sqlite is a C library, for a binding, it uses CGO. But I am not a fan of CGO, since I cross compile the app from my x86 main machine to my arm raspberry pi, using CGO will lead to some stupid error when the remote libc version doesn't match. And using musl in Gentoo is kind of tedious. And there is a CGO free sqlite library. And I am happily using it.

But with the switch to sqlite, the original migration tool, tern, is no longer usable, because it is just for PostgreSQL. I switched back to the good old migrate, which was the first migration tool I knew back in the days, when I was learning Go from the book Let's Go.

Tailwind to pure css

Tailwind was very good for visualizing incremental changes, but it pollutes the html file. And I have to copy the same thing over and over again. I was even using a component library built on top of tailwind like daisyui. They make things much more complicated than it needs to be.

The last time I was writing a pure CSS file was in 2020, at that time, scoped CSS was not a thing but a proposal. Nowadays, most of the browsers supports scoped CSS, which is a game changer for me to go back to using pure CSS.

With the help of AI, even the free tier Grok. Writing pure CSS is no longer a nightmare.

Chi to net/http (NOT work)

Since Go version 1.22, the built-in net/http support path parameter and method. If you are a long time go user, you may remember how awkward the original way of doing method dispatching.

In theory, chi is no longer necessary.

After spending a whole afternoon tinkering with it, I decided to go back to Chi. The built-in mux has a worse syntax of specifying http method, it directly embeded into the path string as a prefix. Like this.

// Built-in
mux.HandleFunc("GET /path/", handler)

// Chi
r.Get("/", app.Home)

The regex handling is different, in Chi, the path is absolute. It routes to the thing you specify, and that's the only thing. But in built-in mux, you have to put a $ sign at the end of it. Otherwise it will match like this.

r.Get("/*", app.Home)

Lastly, middlewares, the final straw. We can enable middleware very easily in Chi like this.

r.Use(cors.AllowAll().Handler)

Without Chi, we are going back the the parenthesis hell of nesting handler. Somehow reminds me of writing lisp.

sqlc (NOT in use)

Generating go code from sql is the standard practice. I was using a handmade sql transation fully written by hand. Now I decided to use a cli tool for that, sqlc.

That, does not work for me. sqlc generates a struct for each sql statement. I personally don't like that. I like to use the schema struct as the primary source when doing CRUD actions.

Also, it requires me to write sql statements. In my use case, almost every schema has a somewhat the same basic CRUD pattern. I would like to directly generate them from the schema provided. For complex queries or updates, I might just write the Go code myself. Does not need the help of sqlc.

Thus, I wrote my own generator, again. (I believe this is the third time. The first time I was a Rust fanboy and I decided to write it in Rust to show off my skill. The second I realize how stupid I was and wrote it back in Go. This time, I got the Jai compiler and the code is written in Jai, just for practise.) You may checkout sql2godb. Sadly, if you does not have a Jai compiler, the code is no use.

Htmx4 cors issue

Fun fact, the original service I was using requires cors between my two servers. And htmx4 somehow had a bug that makes cors not working. My first reaction was to complain on Reddit. The issue was solved after roughly a week. Thanks to the developers of htmx.