On security

On security

Bia is a food management tool for people with chronic GI issues. It has two different apps: desktop and mobile, a backend, and an admin panel. Someday, the user will be able to log their symptoms, which is not only health data but also really intimate data.

This is why security is a first-class concern. I want users' data to be as safe as I can make it, even if they self-host Bia. And that's the reason behind most of the stack choices I've made.

Is this post going to sound like overengineering? Yes. Especially if you’re used to startups, going fast to launch quickly, and vibe coding a new tool every month. Which is totally valid and the right way to do things for some projects. It’s simply not the paradigm this project lives in.

Security of the stack

A desktop app

A web app, which is an application that gets accessed via the browser, would have been the easier path — it's familiar territory, pick a JS framework, and you're moving within hours. Security isn't actually the main reason I went for a desktop app that you have to install and open in its own window, but it's what I want to talk about today.

The threat model I'm designing against isn't sophisticated attackers or data breaches. It's: "my housemate was using the computer, and I'd left my session open." That might sound trivial, but GI symptoms are genuinely intimate data, the kind most people struggle to discuss even with their doctor. For that, Bia will use the OS lock as its session guard: Windows Hello, Touch ID, Linux PAM. The device you already trust, doing the job it was built for.

Why Tauri…

… and not Electron

Electron is the most common way to build cross-platform desktop apps with web technologies. It works by bundling a full copy of Chromium — the browser engine behind Chrome — along with a Node.js runtime, into every app you ship. That's how VS Code, Slack, and Discord are built.

It works, but it comes with a cost. An empty Electron app is over 100MB before you've written a line of your own code. More relevantly for Bia: you're shipping a full browser engine and a JS runtime, each with its own vulnerability history and attack surface, to every user.

Tauri takes a different approach. It uses the OS's native webview instead of bundling Chromium, and Rust for the backend instead of Node. The result is a much smaller binary and a significantly reduced attack surface — without giving up the ability to build the frontend with standard web technologies.

… and not Dioxus

Dioxus is a Rust UI framework. With its desktop renderer, you get a similar setup to Tauri: native webview, no bundled browser, but instead of writing JS for the frontend, you write everything in Rust.

My first attempt used Dioxus. I scrapped it within days: the problem was testability. I couldn't find a good strategy for testing the component layer or running end-to-end tests. I spent two days trying. When I couldn't get there, I moved on.

I briefly considered Tauri with Leptos, or Tauri with a JS framework other than Svelte. I'd already built the prototype in Svelte and knew I could be productive with it immediately. Adding another unknown felt like the wrong trade.

Tauri with SvelteKit turned out to be surprisingly pleasant. I followed the documentation, and it just worked. The IPC between Rust and the frontend is largely transparent, the SvelteKit integration needs almost no ceremony, and the combination ended up feeling less like a compromise and more like a good fit.

Rust

The backend could have been written in anything. A tool people open a couple of times a day, niche user base, straightforward data model… performance was never going to be the bottleneck. Language choice was free.

But data breaches, though not my primary threat model, are still a real risk, and that's where Rust earns its place. Shipping code that structurally cannot have memory allocation bugs, not through discipline or careful review, but because the language itself makes them impossible, is probably the strongest mitigation available to a solo developer. No exploit surface means nothing to patch after the fact.

Then why not JS, Java, C#, or another memory-safe language? The initial Dioxus choice for the frontend made Rust the logical choice for the backend. But had I started with Tauri/svelte right away, I would still have chosen Rust over JS, by simple preference.

Security inside

Row Level Security

When multiple people use the same application, the database usually holds everyone's data in the same tables. The question is: what stops user A from seeing user B's rows? The common answer is application code: a filter somewhere that says "only fetch rows belonging to the current user." It works until it doesn't: a bug, a missing filter, a compromised dependency, and data leaks across users.

Bia uses a different approach. Every row of user data is tagged with a user ID, and the database itself enforces that you can only read or write rows that match your ID. The application doesn't get a chance to forget. There's no filter to accidentally omit. The constraint lives a layer deeper than the code.

I hadn't used it before Bia, but Postgres makes it fairly approachable. Set it up once for the tables that hold user data, wrap your calls with a helper that sets the user ID, and you're done. You stop thinking about it at the query level because you don't have to think about it anymore — the database has it covered.

The auth pipeline

In the world of authentication, the current state of the art is OpenID Connect, or, very simplified, the Facebook and Google buttons. I hadn’t implemented it before, and even if I had, it is very easy to get it subtly wrong. So I decided to use a battle-tested, ready-made solution instead: Keycloak, and focus my time and energy on its integration, a much narrower puzzle.

The flow looks like this:

  1. Tauri asks the server for the auth URL
  2. Tauri opens that URL in the user’s browser
  3. The user authenticates through Keycloak
  4. Keycloak and the server perform the token exchange
  5. The server streams the tokens back to Tauri via a gRPC streaming response
  6. Tauri stores them in memory and in the OS keychain

Why not handle the token exchange directly in the frontend, as a standard PKCE flow would? You can't easily callback into a Tauri app from an external browser, as deep linking across browsers isn't reliable enough to depend on yet. Routing through the backend sidesteps both problems, and as a bonus, tokens never touch the JS layer at all. They land in Rust memory and the OS keychain, which is precisely where you need them.

It's still the most architecturally complex piece of Bia. Getting it right took longer than I'd like to admit.


Security isn’t a one-time decision, and sometimes it means choosing the complex route. There will be more decisions impacted by this core value in the future. I'll get to them in time. Is it overengineering? Maybe. But when you're asking people to trust you with data they can barely discuss with their doctor, doing your best to deserve that trust doesn't feel like too much.