PullMD Part 6: The Multi-User Pivot – Simply Explained
At the end of Part 5, one item was sitting at the top of my to-do list: build a system where the tool can handle more than one user. I hadn’t planned this for now. I’d parked it for some later phase.
But on the evening of April 29, I sat in front of the list of open requests and realized I didn’t really have a choice.
Why This Jumped to the Front
The reason wasn’t the multi-user item itself. It was the two items right after it.
One asked for a way to log in through a standard handshake that browsers use to connect apps safely. Another asked how Claude Desktop — the app version of Claude — could connect to a copy of my tool running on someone else’s server. Claude Desktop only knows that one handshake.
Both of those needed something simple: a notion of who is using the tool. Without users, there’s no handshake. Without the handshake, neither request can be answered.
What I’d considered “nice to have” turned out to be the thing both other items were quietly waiting for. I hadn’t seen that before.
So the order wasn’t really a choice. It was just dependencies. I started that evening.
The Plan: Three Modes
The most important rule for the update was: don’t break anyone who’s already running this thing.
Since I’d posted about my Markdown service on Reddit at the end of April, a few hundred folks had downloaded their own copy. If they ran the usual “give me the newest version” command, they shouldn’t suddenly be locked out.
So I built three modes:
- Off (the default) — works exactly like before. No login, no accounts, nothing new to configure.
- One-admin — a single user with a password, no public sign-up. For someone running their own private copy.
- Multi-user — multiple accounts, people can register themselves. For shared servers.
If you change nothing, you get “Off”. If you want the new system, you set three lines in your config file and restart.
What Actually Changed Under the Hood
The git history looks long — about thirty small steps between the old version and the new one — but a lot of that is tests and documentation. The real new part is a kind of gatekeeper layer and five new tables in the database.
Passwords: I used a method called Argon2id — a way to scramble passwords so that even if someone steals the database, they can’t read the originals. I dialed the settings above the standard recommendation.
Sessions: When you log in, the tool gives your browser a little ticket. The ticket is locked so other websites can’t read it, and it quietly extends itself for a week as long as you’re using it.
Personal access codes: For scripts and tools that need to talk to your account, the tool can generate a long random code. Only the first few letters are visible afterwards — enough to recognize which code is which, but not enough to use it.
A rescue tool: A small command-line helper for the case where someone forgets their admin password.
The whole gatekeeper layer was designed so that the next phase — that browser-app handshake — can be added later as a new path, without rewriting what already works.
The Security Review I Published On Purpose
Before I shipped the new version, I asked a separate helper to play security reviewer and pick everything apart. It found one serious problem and two smaller ones. All three got fixed before release.
The serious one was real: when the tool gave you a new personal access code, it had been showing the code in the address bar — meaning the code ended up in your browser history and possibly in server logs. Fix: show the code once on a normal page, then forget it. The address bar stays clean.
After those fixes, six smaller suggestions stayed open — things like “the login page should have a speed limit so nobody can guess passwords forever” or “this random ID could use more bits”.
I kept all six items documented in a file inside the public code, called something like “Review Findings”. Anyone who installs my tool on their own server can read this file in five minutes and decide for themselves whether any of those points matter for their situation.
Instead of quietly fixing some and hiding the rest: let the people who run this thing decide what their own risk tolerance is. Honesty over polish.
A Tiny Privacy Slip in the Release Notes
While writing the announcement for the new version, the internal address of one of my private test machines accidentally slipped into the public text. A computer name that had no business showing up anywhere outside my own network.
I fixed it within minutes of publishing. No real damage.
But I had no automatic check for that. A small guard that scans release text before it goes out would have caught it. That’s now on my backlog.
Release notes deserve the same care as the code itself.
Why the “Latest” Label Didn’t Move Yet
Normally when you publish a new version of something, you also point the label “latest” at it. So that anyone who installs “the latest version” gets the new one.
I didn’t do that this time. On purpose.
Anyone who’d configured their install to grab “latest” automatically kept getting the old version for two more weeks. The new login system doesn’t appear overnight on someone’s quiet little setup that they’d been running without any login at all.
If you actively wanted the new version, you could ask for it by number. The “latest” label gets moved on a scheduled date — and only after a manual review.
Tying my own hands against the temptation to push the new thing too eagerly.
A Bug, and Three Wrong Tries at One CSS Line
After the release, the first admin who installed it got in touch. Their history page was empty even though everything had been saved. The migration script had updated one table correctly but completely forgotten a second one that links saved items to the user who saved them. Classic “worked on my machine because my test data was tidier than reality” problem.
I shipped a fix the same hour. The fix was written so it can run multiple times without making a mess.
Smaller, more embarrassing drama: the login page. Three wrong attempts at the styling before I noticed that an old rule from a different page was making every button stretch to full width. That’s why my layout kept collapsing. One line in the styles, three tries to find it.
Three attempts for one line of CSS isn’t a defeat. It’s also not the most elegant path.
The Real Lesson
The new login system itself was less work than I’d expected. Four days from the old version to the new one — including the security review, the tests, and the documentation.
The actual lesson wasn’t the code. It was this: publish the security review with the release. If six small concerns are still open, the people running this thing should know about them. Not just me.
What’s Next
The new version shipped on the evening of May 2. The next logical step was the browser handshake — the thing the original two requests had been waiting for.
The branch was ready. What happened next was one of the most frustrating debugging stretches I’ve had in a long time. And the frustrating part wasn’t a mistake I had made.
Part 6 of 9 in the PullMD series.
← PullMD Part 5: Reddit Day and First Issues | PullMD Part 7: Spec-Compliance Isn’t Enough →