What building our first apps taught us
Lessons from making our first five apps, serve, Exif AI, dskcopy, idNX and Grabbr: small scope, one-liners as specs, cross-platform costs, code signing, support and saying no.
We have made five apps of our own so far: serve, Exif AI, dskcopy and idNX, with Grabbr still being built. Building small tools as a small studio teaches you things that web work mostly hides, about scope, platforms, trust and the people on the other end. This post collects the lessons from building our first apps that we keep coming back to. They are general on purpose. Each one showed up in more than one app, and none of them is a clever trick.
Four of them live on the desktop apps page. idNX is a network tool, so it sits with our security tools. Each has a single line that describes it, and that line turned out to be the first lesson.
The one-liner is the spec
Every app we make starts with one sentence. serve is "serve a folder over HTTP or HTTPS with one native binary." Exif AI is an "AI-powered EXIF metadata writer." dskcopy is a "fast, safe disk image writer." idNX is "know your network. Not just its IPs." Grabbr, which is not finished yet, is "download video and audio without the clutter."
These read like taglines, but we use them as specifications. When a feature idea comes up, the first question is whether it serves the sentence. A deployment pipeline does not belong in serve, however useful it is. A panel that adds clutter does not belong in Grabbr, because the sentence says so in plain words. A feature that reads a drive back into an image file does not belong in dskcopy, because the sentence says writer.
A one-line spec is incomplete, of course. It does not tell you how to handle a full disk or a broken network. What it does is give everyone the same test for new work, including the person who wrote it three months ago and has since fallen in love with an idea. Longer specs tend to grow to fit the features. A single sentence resists that.
Writing the sentence is also the cheapest way to find out that you do not yet know what you are building. If it takes a paragraph to describe the app, the app is probably two apps.
Small scope is a feature
All five apps are small. They do one job each, and they try to do it without asking for attention. That was a choice, and it has shaped almost everything else.
Small scope makes an app easier to finish, which matters more than it sounds for a studio with a handful of people. It makes the interface easier to learn, because there is less of it. It makes the code easier to keep secure, because there are fewer paths through it and fewer dependencies behind it. And it makes the app easier to explain, which is most of marketing for a small team.
The cost is that someone always wants the app to do the next thing. A folder server could also be a deployment tool. A disk image writer could also be a backup tool. A network mapper could also be a monitoring system. Each step is reasonable and each one makes the app a little less itself. We have learned to write those ideas down, leave them alone for a while, and see if they still seem necessary later. Most do not.
Cross-platform costs more than it looks
All five target macOS, Windows and Linux. serve and idNX are single binaries. Exif AI and dskcopy each come as a desktop app and a command-line tool, exif-ai-cli and dskcopy-cli, built from the same code. Grabbr is meant for the same three systems once it is ready.
Cross-platform frameworks make it easy to put the same window on three systems. They do not make the app feel native on all three. File dialogs, keyboard shortcuts, menus, notifications, where settings live and how an app starts at login all differ, and users notice when an app gets them wrong. The first build on a new platform is quick. Making it feel right takes much longer.
Anything that touches the system goes further. Disk access, file permissions, paths, removable drives, network interfaces and background processes behave differently on each platform, sometimes in ways the documentation does not mention. An app whose whole job is the system, like a tool that writes images to USB drives or one that reads the local network, has to know each platform deeply rather than politely. That is a real argument for staying on one system. We chose three anyway, and the price is paid in testing.
Our rule of thumb now is to count every additional platform as roughly a new product for testing, packaging and support, even when the code is mostly shared. If we would not build that second product on its own merits, we probably should not ship the port.
Signing and trust
A desktop app asks for more trust than a website. It runs with the user's permissions, it can read their files and it stays on their machine. Operating systems know this, and they put real checks in front of unsigned software.
On macOS that means signing with an Apple Developer ID and sending each build to Apple for notarisation, otherwise Gatekeeper will warn users or block the app. On Windows it means Authenticode signing, and even then SmartScreen may warn about a new file until it has built up reputation. Linux has no single model; packages, repositories and formats each handle trust their own way. Plain downloads need something too, which is why we publish SHA-256 checksums and our install scripts check them.
We learned to treat signing as part of the build, not a step before release. The aim is that every build leaving CI is signed and, where the platform expects it, notarised, with keys held in the build system under tight access rather than on a laptop. When signing is automatic, nobody is tempted to skip it for a quick fix, and a user never meets a scary dialog because we were in a hurry. We wrote more about the build side in our post on supply-chain safety for small teams.
Signing also changes what you promise. Once your name is attached to a binary, that binary is yours for as long as anyone runs it. It is a good reason to keep apps small.
Support is part of the product
Web apps can be fixed for everyone at once. A desktop app runs on machines you have never seen, with different versions, different settings, and other software you have never heard of. When something goes wrong, the only view you have is what the user tells you.
That has made us careful about a few things. Error messages should say what happened and what the user can do next, in plain words, never just a code. Logs should be easy to find and safe to share, with nothing private in them. The version number should be one command or one click away. And each app should have an obvious way to ask a question, because a question you can answer quickly is far better than a review you cannot reply to.
Support also feeds back into scope. The questions people ask show you which parts of an app are unclear. More often than not, the fix is to remove an option rather than add a help page.
Saying no
The last lesson ties the others together. Almost every request we receive is reasonable. People want an extra format, another platform, a sync feature, an option for a workflow we had not considered. Saying yes to each one feels generous, and after a year it produces an app nobody would have designed on purpose.
We try to say no politely and with a reason, usually the one-liner. Sometimes the right answer is a different tool, and we are happy to point people to one. Sometimes a request comes up often enough, from enough different people, that it clearly belongs, and then we build it properly. The difference between those cases is rarely obvious on the first request. Waiting is often how you find out.
Saying no to ourselves is harder. Engineers enjoy building, and an extra feature is fun to write. The one-liner is as much for us as for anyone else.
If you are planning a desktop app of your own and want to talk through scope, platforms or signing, our custom development work starts with the same one sentence. We usually ask for it in the first meeting, and it is often the hardest thing to write.