All notes

Sep 2026 · 4 min read

Published

Build Log #11: The Key the App Never Writes to Disk in Plain Text

AuthorKervintz Noel
Filed underAI Engineering · Claude Code · Building in Public · TypeScript
SeriesPart 11 of 11 · View the full case study

Before writing a line of the Accounts or Settings screen, I went looking for what already existed around API keys and raw PDFs. Neither answer was good.

LLM provider keys had been environment-variable-only since the Privacy Gateway post — there was no settings screen, so nothing had ever persisted one. Worse: raw PDF cleanup didn't exist at all. Every file accepted since the very first Import screen had been written to a temp folder and never deleted. That's not cosmetic — the requirements treat retention as a security requirement, not a nice-to-have, and I'd been quietly failing it for three posts.

A key that only ever exists encrypted

The backend can't decrypt anything itself — it's Python, and the encryption is Electron's safeStorage, which is Node-only. So the design hands the backend something it already knows how to use: an environment variable at spawn. Electron encrypts the key into its own settings file, decrypts it in memory when it starts the backend, and injects it as the same env var the code already read. The Python process never sees a settings file and never persists a secret of its own.

Saving a new key restarts the backend. A few seconds of local downtime beats building a live settings endpoint for a screen most people touch once.

Accounts, on purpose, does nothing extra

The design notes describe an Accounts screen where you can merge two mis-grouped accounts. I built the screen and left that button out, because the schema makes the problem it's meant to solve impossible: an account's identity — bank, account type, masked digits — is a unique constraint at the database level. Two rows that are actually the same account can't exist. Building a merge feature for a bug the architecture already prevents would have been solving a problem I didn't have.

  1. 01Settings: provider choice, a masked key field with a real Test Connection call to the provider — an actual API request, not a fake ping
  2. 02Retention toggle, now actually enforced: a statement's raw PDF is deleted once its job reaches a terminal status, unless retention is on
  3. 03The category-rule table from the Privacy Gateway post, now editable directly instead of only reactively through Review

I tested the key flow with an intentionally wrong Anthropic key. Test Connection made a real call, came back with the real HTTP error, and the key stayed masked in the UI the entire time — never echoed, never logged. What I couldn't verify from here: the actual safeStorage encrypt/decrypt round-trip, and that a saved key survives restarting the real Electron app. Browser automation can't drive that; it's written up as a manual check for me to run in the real app before this merges.


Where build-plan #9 ends

This closes it. Import, History, Dashboard, Review, Accounts, Settings — all six screens are real, not placeholders. The known gap I'm carrying forward: the built-in merchant rules are thin, so a first import still leaves a large review queue. Worth fixing before anyone but me ever sees this app.

Every screen in the plan exists now. What's missing isn't a screen — it's a Windows installer someone else could double-click.

Next

Build-plan #10: packaging. Turning two dev processes I run by hand into one installer that works on a machine with no Python and no Node on it. Not started yet.

I write these as I go. You can follow along here or on Hashnode, where I'll start cross-posting.

All notes