For about ten years I have been saving things from the internet. Articles mostly, but also videos, tweets, and the occasional PDF. It started in Pocket and moved to Readwise Reader a few years ago. The pile is now 1,858 items and about 5.9 million words, which works out to roughly 79 books. I have read a small fraction of it.
I wrote a while back that I have lists of books I want to read all over the place and can never find them when I need them. This is the same problem, only bigger, with the added wrinkle that the whole collection lived in somebody else's database. Pocket shut down last year. It will not be the last.
So I built my own. It's a small web app called afile. It captures things, organizes them, searches them, and as of this week, lets me ask questions of the whole collection. Like this website and VeloGPS, I built it with Claude Code. Here is how it came together.
What I wanted
A few decisions up front made everything else easier.
It is for one person. There is one password and one user - me. No accounts, no analytics, no social anything. I can publish an individual item or a board if I want to, but that is the extent of the sharing.
The whole database is one file. Everything lives in a single SQLite file. A backup of production is a copy of one file. Pulling production down to my laptop to work on it is a copy of one file. After watching a lot of services come and go, there is something very comforting about a file called data.db sitting on my own disk.
Saving has to take seconds. If it takes longer than that I won't do it, and then the whole thing is pointless. So there are five ways in: paste a URL, a bookmarklet, the iOS share sheet (it installs as a web app on the phone), a file upload, and a Telegram bot. The bot gets the most use. Forward a link from my phone and it's saved.
What it's built with
Nothing exotic.
- Next.js 15 with React 19 and TypeScript. One app serves the login-gated UI, the JSON API, and the public share pages.
- SQLite via better-sqlite3, with FTS5 for full-text search.
- Drizzle ORM for the schema. The search index is raw SQL.
- Readability, the same library behind reader mode in Firefox, pulls the article text out of a page and stores it for good.
- Claude. Haiku writes a summary and tags for every item that comes in. Opus powers the chat.
- Caddy and systemd on a small DigitalOcean droplet.
How it works
When I save a URL, the item shows up right away and a background job does the slow part: fetch the page, pull out the readable text, grab the metadata and a thumbnail, then ask Claude for a two sentence summary and a handful of tags. The result is a wall of cards that I can filter by type and tag, and search across titles, notes, and the full text of every article. That last part matters. Most of what I saved was saved because of something in the body of the article, not the headline.

The library. Each card gets a summary and tags when it comes in. The tag chips along the top are the actual vocabulary of the collection - not categories I made up ahead of time.
For reading there is a view that works like a mail client. Sortable list on the left, reading pane on the right, arrow keys to move between them. Because the article text is stored at capture time, a paywalled piece I saved in 2016 is still readable even if the page is long gone. I can highlight while I read, and the highlights collect into their own page, which is as close as I have come to keeping a commonplace book.

The reader. The list on the left, the stored article text on the right, with the summary up top.
And then there is the stats page, which is where the whole exercise gets a bit humbling. 1,858 things saved since February 2015. 5.9 million words. Of the things I saved in order to read later, I have finished 1%. The page also helpfully calculates that the remaining 99% is 494 hours of reading. I am choosing to view this as a well stocked library rather than a to-do list.

The stats page. Eleven years of saving, 494 hours of reading to go.
Asking the library questions
This is the part I am most excited about, and it only landed this week. I can now ask a question across the whole collection. What advice about parenting have I saved most often? What are the productivity ideas that keep showing up? What have I collected about Amazon over the years?
Under the hood it is a loop. Claude gets three tools: search the full-text index, read a single item in full (including my highlights), and list the collection's tags. It decides how to use them. For a question like the parenting one it runs four or five searches with different wordings, skims the results, reads the strongest matches all the way through, and then writes up an answer where every claim links back to the item it came from.

Asking the library about parenting. The gray lines at the top are the searches and reads it decided to run on its own. Every citation is a link to the item in my library.
Watching it go is a little strange. It pulled up an essay on Quaker parenting and a New York Times piece about raising kids in other cultures that make essentially the same point about autonomy. I saved them eight years apart and had never connected them. It turns out ten years of "this is interesting" clicks make a pretty good dataset once something can actually read all of it.
Getting ten years of data in
Building the app was the easy part. The hard part was the archaeology. My history was spread across a Pocket export and a Readwise Reader export, with a lot of overlap and inconsistent metadata between them. I wrote import scripts to map both into one schema, and then a series of backfill scripts to fill in the gaps: extract article text for the old saves, fetch metadata and thumbnails, run the summaries and tags over everything, and pull transcripts for YouTube videos so those are searchable too.
My favorite bug from this process: the two exports disagreed about URLs. Pocket had saved everything as http:// and Readwise as https://. The de-duplication keys on the canonical URL, so it walked right past 95 pairs of the same article. A merge script sorted it out - keeping whichever copy had the richer content, combining the tags, and preserving the original save date back to 2015.
Decisions I'd make again
One process, no services. The background job queue is a table in SQLite, drained by a worker that runs inside the Next.js process. No Redis, no cron, no message broker. "Web scale" for this app is one person clicking save a few times a day.
The search index maintains itself. FTS5 is kept up to date by SQLite triggers, not application code. Insert or update an item and the index follows along. Searching 5.9 million words comes back in milliseconds on a $6 virtual machine.
One list of what a thing can be. Every item type (article, video, tweet, email, note, image, file) is defined in a single registry file with its traits. The capture pipeline, the cards, the filters, and the AI classification all read from it. Adding a type is one entry plus whatever behavior is actually new.
The AI is a layer, not the foundation. Every AI feature degrades gracefully. No API key means no summaries and no chat, but capture, search, and reading work exactly the same. The core of the app doesn't depend on anyone's model.
Never trust a URL. The server fetches whatever URL I hand it, which is a textbook setup for server-side request forgery. Every outbound fetch goes through a guard that resolves the DNS and checks each redirect hop against private address ranges before following it.
Where it runs
The whole thing runs on the smallest DigitalOcean droplet: one shared CPU, 1GB of RAM, $6 a month. Caddy sits in front and handles HTTPS on its own. A deploy is one Makefile target. Push to git, pull on the droplet, build there so the native modules match the server, restart the systemd service. That's the entire infrastructure. One VM, one domain, nothing else.
The 1GB of memory keeps things interesting. The article extraction library is hungry enough that the big backfills run on my laptop and the results get shipped up as SQL. That constraint feels right for a personal tool. And because the database is one file, the backup strategy is make sync-data, which pulls an online SQLite backup down to my laptop. That same file is my local development database. Real backups, production data on my machine, and a dev environment, all from one command.
What I learned
Owning the data changes what you do with it. When the archive lived in someone else's app it was write-only. Save and forget. Now that it's in a schema I control, I keep finding new things to do with it: stats, highlights, transcripts, chat. Nothing about the data changed. I just got to touch it.
SQLite is enough. Full-text search across millions of words, a job queue, and eleven years of data. One file, no database server, backed up with cp. Most personal projects need a lot less database than people reach for.
Plain search plus a good searcher beats fancy search. I assumed the chat feature would need a vector database and embeddings. Instead, handing Claude an ordinary keyword search and letting it pick the queries works surprisingly well. It searches the way a good librarian does, trying synonyms and following leads. Sometimes the boring index is the right one.
Save things. Every one of those 1,858 items was a moment where something caught my attention. I had no idea I was building a dataset for something to read back to me, because nothing could read it back to me at the time. Storage is cheap. Keep saving. Future you will have better tools than you can picture today.
Now I just need to read the other 99%.