In this essay
For six years I paid for an RSS service that did everything. Folders, saved searches, tagging rules, keyboard shortcuts, a lovely mobile app, even a feature that recommended new feeds based on what I’d read. It was a beautiful piece of engineering, and I opened it perhaps twice a month, each time greeted by a counter reading 1,847 unread.
Last month I deleted my account and wrote my own replacement. It’s about 400 lines of Python, it runs on a tiny server, and I now read more than I have in years. This is the story of what I built, what I deliberately left out, and why I think small, personal software is underrated.
The problem wasn’t the software
It took me embarrassingly long to see it. The reader wasn’t broken; my relationship to it was. A number like 1,847 is a debt. Every time I opened the app I felt behind, so I closed it again. A tool that makes you feel guilty is a tool you’ll avoid, no matter how many features it has.
What I actually wanted was much simpler: a single page that shows me what’s new from the fifty or so sites I like, lets me read it, and then lets me stop. Not a backlog to clear, just a window to look through.
Setting constraints
Before writing any code, I wrote down the rules. Constraints are the best design tool I know, because they make the decisions for you later:
- One file of code, under 400 lines.
- One SQLite database, with nothing else to host or configure.
- No accounts, no sync, no mobile app. Just a web page I can open on any device.
- No unread counts, ever.
- Items older than two weeks quietly disappear.
That last rule is the most important one. It reframes reading from “clearing a backlog” to “seeing what’s new”. If I miss something, I miss it. The world goes on, and so do the feeds.
How it works
The whole thing has three parts. A fetcher runs from cron every thirty minutes, pulls each feed in a list, and stores new items in SQLite. A tiny web server renders the most recent items as a single plain HTML page, grouped by day. And the list of feeds lives in a text file I edit by hand. That’s it. Here’s the heart of the fetcher, simplified:
import feedparser, sqlite3, time
db = sqlite3.connect("reader.db")
db.execute("""CREATE TABLE IF NOT EXISTS items (
url TEXT PRIMARY KEY, title TEXT, source TEXT, published INTEGER)""")
for line in open("feeds.txt"):
feed = feedparser.parse(line.strip())
for entry in feed.entries:
published = int(time.mktime(entry.published_parsed))
db.execute("INSERT OR IGNORE INTO items VALUES (?,?,?,?)",
(entry.link, entry.title, feed.feed.title, published))
db.execute("DELETE FROM items WHERE published < ?", (time.time() - 14 * 86400,))
db.commit()The primary key on the URL does the deduplication for free, and the final delete statement is the “quietly disappear” rule expressed in a single line. I’m a little proud of how little there is. Almost every line is doing something that matters, and there’s nowhere for bugs to hide.
The gotchas nobody warns you about
Feeds are messier than you’d expect. Some have no publication date, so you fall back to the time you first saw the item. Some change their URLs when a site migrates, which creates duplicates. Some return errors for a week and then recover, and your fetcher should shrug rather than crash. A few serve the entire article in the feed while others give you only a headline, so the page needs to cope with both.
I solved most of this by being tolerant. Each feed is wrapped in its own try block; if something goes wrong, I log it and move on to the next. I also learned to respect conditional requests. Sending the ETag and Last-Modified headers back lets a server answer “nothing has changed” without resending the whole feed. It’s kinder to the people running small sites, and it makes the fetcher noticeably faster.
What I deliberately cut
- Folders. A flat list of fifty feeds is perfectly manageable when there are no unread counts to feel guilty about.
- Search. If I want to find something I remember, I use a search engine.
- Starring and tagging. When something deserves to be kept, I email the link to myself.
- Read and unread state. The entire concept. This one felt radical, and it was the best decision I made.
Every feature I removed was one I could have built in an evening. That’s the trap of writing your own tools: you can add anything, so you have to be strict about what earns its place. I keep a short list of “things I might add” and, so far, I’ve never needed to look at it.
How my reading changed
Within a week I was reading more. Not hugely more in volume, but more in attention. The page is short enough to finish. When I reach the bottom, I’m done, and being done is a feeling I hadn’t had with a feed reader in years. I read the long pieces properly now rather than skimming them to make a number go down.
There’s also something satisfying about opening a page and knowing exactly how it works. When the layout annoyed me, I changed three lines of CSS. When a feed misbehaved, I read the fetcher’s log and fixed it in five minutes. That sense of ownership is hard to describe if you haven’t felt it, and it’s a big part of why small software is worth the effort.
Lessons for building small software
- Start from the feeling you want, not from a list of features.
- Put a ceiling on the size, and respect it when it gets uncomfortable.
- Choose boring storage. A single SQLite file will outlive most trends.
- Make deletion a first-class feature. Things that expire can’t pile up.
- Build it for exactly one user, and let yourself be that user.
It’s quite possible this reader is useless to anyone but me, and that’s precisely the point. Most software is built to be everything to everyone, which is why it’s so often a little wrong for each of us. A few hundred lines, written on a quiet weekend, can fit like a glove. If you’ve been quietly annoyed by a tool you pay for, it might be worth asking what the smallest version of it would look like. You might be surprised how small that is.