There are a handful of small, recurring annoyances in my daily workflow that I've just learned to accept. One of the biggest has always been the ridiculous dance required to get a photo from my phone to my PC, or a snippet of text from my browser to my phone. The usual solutions have always irked me: emailing myself, finding a USB cable, or using a bloated cloud service that takes forever to load (and then it asks you to do 2 factor authentication). Come on.

This is the kind of inefficient process that drives an engineer crazy. So, I decided to fix it. I carved out a weekend to build "Paste": a private, self-hosted web app to solve this one problem, and solve it well.

The goal was extreme simplicity. A big text box for copy-pasting. A dropzone for files. That's it. The interface needed to be clean, fast, and completely responsive, working just as well on my phone as on my desktop. I built the first version in a couple of days. It had the core functionality, a clean UI, and it felt great to use.


From Prototype to Polish

Who am I kidding. Of course I'm going to over-engineer the crap out of this.

Building the prototype is always the easy part. Making it something I'd actually trust with my own files took considerably longer. This is a shorter post than usual -- just a few things I hit along the way.

First up was security. In a moment of self-auditing, I realized my file upload logic had a glaring hole. If a malicious user (there's no other user, but pretend) tried to upload a file named ../../some/other/path, the server would happily obey, potentially overwriting critical system files. This is a classic Directory Traversal vulnerability. The fix was simple: strip every incoming filename down to a name and nothing else, so there's no path left to traverse.

Next came a file size limitation that caught me by surprise. I dragged a 5MB video file onto the page to test an upload. It failed instantly with a Request Entity Too Large error. This was confusing, as I had set the application's own upload limit to 1GB. After a careful series of tests to find the root cause, it turns out the problem wasn't in my code implementation at all. It was the Nginx reverse proxy sitting in front of the application, which has its own, much smaller default limit to prevent denial-of-service attacks. A quick tweak to the Nginx config taught the proxy to allow larger files through to the app. It's a good reminder that application code itself is only one part of the stack.

Finally, there was the user experience polish. The initial design was a single, democratic grid where a five-word text snippet held the same visual importance as a 500MB video file. It was definitely functional, but after using it for a while I didn't like it. The primary use case (quick text pasting) was getting buried. The fix was to rethink the entire information architecture. I decided to split the UI into two distinct sections: a high-priority area for "Text Snippets" at the top, and a separate grid for "Files" below. To reinforce this, I gave their headers a subtle color-coding: a muted yellow for text snippets (think sticky notes), and a desaturated steel blue for files. Small change. It's the difference between a page I scan and a page I search.

The Paste web app. A full-width text box sits at the top with a Save Text button. Below it, a Text Snippets section holds one card with a yellow header containing an nginx reload command. Below that, a separate Files section holds two cards with blue headers, an image and a video, each showing its type and creation time.
The whole argument of the redesign is the vertical order. Text gets the top of the page and a yellow header, files get their own section below in blue -- so the common case is where my eye already is, instead of somewhere in a grid.

And that's it. A weekend to build, and then a while longer making it reliable enough that I stopped thinking about it -- which is the actual bar for a tool you use every day.

I still use it constantly. Emailing myself files feels absurd now.