Supporting an app that doesn't know who you are

By Lior Rabanian · · 6 min read
  • Builder's log
  • Privacy
  • Support

Someone emails to say something is wrong. I open the message and that is everything I will ever have: their words, and whatever they thought to include.

There is no account to look up. No dashboard showing their version, their settings, their last twelve sessions. No error report that arrived before their email did. No record that this person exists, beyond a payment receipt in a system I do not control.

This is the direct consequence of the choices on the front of the site — no account, no telemetry, everything on your own machine — and I want to describe what it is actually like, because those choices get sold as pure wins and they are not.

What disappears along with the account

A user database is not only a privacy liability. It is the substrate half of modern software support runs on.

Gone with it: password resets, because there is no password. Account recovery, because there is no account. "Let me check your subscription status." "I can see the error in your logs." "I've pushed a fix to your instance." Every one of those sentences requires a server that knows who you are, and there isn't one.

Some of that absence is straightforwardly good. Nobody can ever email me and ask me to hand over a customer's notes, because I do not have them. There is no breach that exposes what you write, because there is nowhere for it to leak from. A company that cannot be compelled to produce something has a much simpler relationship with the question than one that promises not to look.

And some of it is just harder. If you lose the file that holds your work, I cannot restore it. There is no copy. That is the same property, read from the other side, and it is why the backup story gets more attention in this app than it does in one where the cloud quietly holds a version for you.

The one thing that replaced logs

You cannot print your way out of a problem in a shipped desktop app. A GUI binary has no console attached — the diagnostic line you carefully wrote goes nowhere at all on the platform where the bug actually reproduces.

So the app writes a small diagnostic file locally. Not analytics; it is never sent anywhere. It is a file on the user's own disk, which the user can read, that says what the app did on launch — so that when someone writes in, there is one thing I can ask them to attach that turns "it did not work" into a sequence of events.

Three constraints on it, all learned rather than designed:

Bounded. 32 KB, capped. An unbounded diagnostic file on a machine you cannot reach is a disk-space bug waiting for the one user who leaves the app running for eleven months.

Best-effort throughout. Every write can fail silently. A diagnostic that can fail a launch is strictly worse than no diagnostic at all, and it is very easy to write the version that throws on a read-only directory and takes the app down with it.

It records the questions, not just the answers. This is the part I got wrong first. Logging what happened is not enough when the interesting case is a step that never ran. If the record shows the app asked a question and got an answer, the problem is in what it did next. If the line is absent entirely, the problem is that it never asked, which is a different bug in a different place. Without that distinction the two are indistinguishable from the outside, and I spent nine round trips with a tester learning that the hard way.

What the app knows about a user: nothing on a server, a bounded local diagnostic file, and a payment receipt held by the processor
The record lives on the user's machine, and only moves if they choose to send it.

Instrument first, then fix

The habit that came out of that: when a bug arrives that I cannot reproduce, the first change I ship is not an attempt at a fix. It is the thing that will tell me which of two explanations is true.

That feels like a delay. It is faster nearly every time, because the alternative — guessing, shipping a plausible fix, and asking a volunteer to try again — costs a full round trip per guess, and a round trip with a real person is a day, not a minute. Three guesses is a week. One instrument is an afternoon.

It has a second benefit I did not anticipate. A fix shipped on a guess, which appears to work, leaves you unsure forever whether you fixed the bug or moved it. A fix shipped against a recorded sequence of events is a fix you can prove. When the bug is in a path that touches somebody's data, "it seems fine now" is not a standard I am willing to ship against — the locked-note race is the case that settled that for me permanently.

Support that happens inside the app

The cheapest support ticket is the one that never gets written, and with no team to answer them, that stops being a platitude and becomes the design constraint.

So the app tries to explain itself at the exact moment it fails. The clearest example: pasting from the clipboard history needs the macOS Accessibility permission, and without it the paste simply cannot happen. The old behaviour was to close the popup and do nothing — which is indistinguishable from broken software, and generates an email that begins "your clipboard doesn't work". Now it names the missing permission, offers to open the right settings pane, and points out that the entry is already on your clipboard so you can paste it yourself. Why a Mac app asks for Accessibility exists for the same reason: the question is predictable, so answer it before it is asked.

Every silent failure is a support ticket with a delay on it. That is the rule, and it has changed how I write error paths more than any other single idea in the project.

The refund policy is an admission

14 days, no reason needed, no proof of uninstall.

That last clause is not generosity. It is honesty about capability. I could not verify an uninstall if I wanted to — there is no phone-home, so there is nothing to observe. A policy demanding proof I cannot check would be theatre, and the sort of theatre that makes a person feel accused on their way out the door.

The same absence shapes what a refund tells me. With no analytics, a cluster of refunds mentioning the same feature is one of the very few real signals I get about what the app is like to use. It is a terrible instrument — tiny sample, self-selected, arriving only from people annoyed enough to act. It is also nearly all of it, along with email, and I would rather work from a biased sample than hold a database of everything everyone does inside their own notes.

That is the trade, stated plainly. I know less about my users than any product manager would consider acceptable, and in exchange the app can promise something almost nobody else in this category can: it makes one network request, ever, and it is a check for a new version. Everything else stays on your Mac, including the record of what went wrong.