Using Rust wasn't a mistake
2025-02-10
It was a painful experience.
I originally created Citadel as a Tauri app, using Svelte for the frontend and Rust in the backend. At the start, this worked pretty well ā I enjoyed learning about Rust and how it worked, managing references, and writing āperformantā software.
At some point, I looked into running the Rust backend as a separate, stand-alone REST API so that you could self host a web version of Citadel. While feasible, it seemed painful. Working with Tokio is annoying for someone whose day job involves writing highly concurrent Elixir and a lot of async Typescript. Why do I have to manage lifecycles? Why am I giving myself this pain? After hours of trying to get async requests working across the app, I gave up. So I shelved the HTTP API and continued working away on Citadel as a desktop app.
Adding features was slow, steady progress. But it often worked in two phases:
- Very quick progress in the UI (using tools I am very familiar with)
- Very, very slow progress in the small, complicated backend
Every time I wanted to retrieve more data from the existing database schema (Citadel uses an existing schema, derived from Calibre), I had to remember how Rust and the libraries Iām using work. Then, I had to figure out what business rules Calibre expects / follows and then mesh the two to get data out ā and the process is even slower when trying to insert new data. All the inherit complexity got lost in the incidental complexity of using Rust.
About a month ago, I came back to Citadel with the plan to start supporting downloading metadata from sources like Hardcover. Calibre supports this (using other providers), Calibre-Web supports this, Citadel needs to. I first tried to make a super-simple prototype in the frontend, but Hardcover blocks requests from localhost, so I had to push it to the backend.
After three hours of trying to get Rust to make a web request in a reasonable, understandable way, I gave up. I doubt Rust is to blame here, but I could not figure out how to use an async runtime like Tokio when making IPC requests via Tauri. The lifetimes got all messed up. I couldnāt find a sync HTTP library, either. I donāt need async HTTP requests! I donāt really care! But the ecosystem has moved to async, and bring your own runtime.
In comparison, the same graphql request in Typescript is trivially easy. I mean, of course it is. There are tradeoffs to that simplicity.
But after looking at the options for Citadel, Iāve landed on migrating the app to Electron. ugh, I know. Electron. Bane of users everywhere, with its non-native UI patterns and 300MB executables. Itās gross! Frankly, I hate it! I started Citadel originally as a Swift app exactly to avoid this problem. I wanted fast, native software. But the pain of not being able to make quick progress because I was learning new tooling outran the joy of learning new tools eventually, and now Iād trade anything to be able to release the features I wanted to add to Citadel over a year ago but couldnāt, because I wasnāt comfortable with my tools.
Iām in do-or-die mode with Citadel. If I canāt start making progress and adding features that I want, Iāll probably stop using it. At that point, who cares if itās built with Electron or Neutralino or Tauri or Wails or if its native software? This is a hobby project, which I wrote for me. I probably have over a gigabyte of intermediate build artifacts for the Rust build. At this point, trading to an Electron build might save me disk space.
So, while I enjoyed the satisfaction and challenge of writing Rust code, Iām dropping the language from Citadel for now. Maybe a new project will come up where Rust feels like the right choice?