HeavyDeets – From Mine to Ours

PIN

HeavyDeets started as a personal thing. I’m tired of constantly being sold to. I’m tired of people owning and selling my data. So, I built my own tracker and self-hosted it. It worked fine for me. SQLite, Docker, done….but I wasn’t done. I wanted to access it on the LAN so I converted the self-hosted repo into a github package and worked up yaml to deploy it to my k3s homelab. Done…..but I wasn’t done.

When I posted the self-hosted repo link, I got a few pms asking how to set it up or saying they were interested in it, but self-hosting wasn’t an option. Ok. I got you. No, I’m not being ultra-altruistic. I’m going to use it to show people, yeah, I can build a scalable production. We all win.

It’s taking some thought.

When an app is designed for only one person, you can skip a lot. You know your own token won’t expire at a bad time. You know you won’t accidentally delete your own data. You don’t need to think about what happens when someone else’s session gets stale, or what you owe a user when they want to change their password on one device without getting booted everywhere else.

So the last few weeks have been about filling those gaps — not new features, just the stuff that has to work before I can hand something to people.

The database schema got rebuilt from scratch. SQLite became PostgreSQL. Nine SQLAlchemy models, Alembic migrations, and a test suite that rolls back at the connection level so tests don’t leak into each other. That last part sounds minor but it’s not.

Auth took the most thought. Access tokens are short-lived JWTs. Refresh tokens are random, hashed, and stored in the database with a TTL that rotates on every use. When you change your password, every other session gets revoked. Your current device stays logged in, everyone else gets kicked out on their next request. I didn’t invent any of this — it’s just the pattern that makes sense once you think through what can go wrong.

There were smaller things too. A CORS misconfiguration that would’ve silently blocked the frontend. I hate CORS. It’s needs to be on a t-shirt. A bcrypt version that broke its own API between releases. An environment variable pointing at the wrong port. None of it is interesting to write about, but all of it has to figured out.

The part I found most satisfying was the token auto-refresh in the frontend. When a request fails with a 401, the app quietly gets a new access token and retries — the user never sees it. If the refresh token itself is expired, it clears everything and sends you to login. It’s invisible when it works, which is what you want.

I’m still building. Payments are in the schema. Infrastructure and deployment are next. But the core is solid now in a way it wasn’t before, and I feel okay about the idea of people I don’t know actually using it….well, I feel better about. Not ok with, but better.