For about ten years I've been saving things from the internet — articles, videos, tweets, the occasional PDF. First in Pocket, then in Readwise Reader. Somewhere along the way the collection crossed a million words, and it hit me that this archive of everything I found interesting lived entirely in other people's databases. So I built my own: a personal web app called afile that captures, organizes, searches, and — as of this week — lets me have a conversation with everything I've ever saved.

Like this website and VeloGPS, the entire thing was built with Claude Code. I wanted to share how it came together, the technical choices behind it, and how it's deployed.

The Idea

The goal was simple: one place I own, holding everything I save, forever. That meant a few design decisions up front:

Single user by design. This isn't a product. There's one password, one user (me), and zero accounts, analytics, or social features. Public sharing exists, but only for individual items and boards I explicitly publish.

The whole database is one file. Everything lives in a single SQLite file. Backing up production is copying one file. Pulling prod down to my laptop for development is copying one file. After twenty years of watching services shut down, there's something deeply reassuring about data.db.

Capture should be effortless. If saving something takes more than a couple of seconds, I won't do it. So there are five ways in: paste a URL, a bookmarklet, the iOS share sheet (it's a PWA), file upload, and a Telegram bot — I forward a link to the bot from my phone and it's saved.

The Stack

  • Next.js 15 with React 19 and TypeScript — one app serves the 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, raw SQL for the search index
  • Readability — the same engine behind Firefox's reader mode — extracts and permanently stores article text
  • Claude — Haiku summarizes and tags every capture; Opus powers the chat
  • Caddy + systemd on a small DigitalOcean droplet

How It Works

When I save a URL, the item appears instantly and a background job does the slow work: fetch the page, extract the readable text, grab metadata and a thumbnail, then ask Claude for a two-sentence summary and topical tags. The result is a card gallery of everything, filterable by type and tag, searchable across titles, notes, and — because the full article text is stored — the complete content of everything I've saved.

The afile card gallery showing saved articles with thumbnails, AI summaries, and tags

The library. Every card gets an AI summary and tags at capture time; the tag chips across the top are the collection's actual vocabulary.

For actually reading, there's a mailbox-style reader view: a sortable list on the left, a clean reading pane on the right, with keyboard navigation. Articles store their extracted text permanently, so a paywalled piece I saved in 2016 is still readable even if the original page is long gone. I can highlight passages while reading, and the highlights collect into a commonplace book.

The reader view with an article list and reading pane

The reader: sortable rows on the left, the stored article text on the right, with the AI summary up top.

There's also a stats page, which is where the collection gets honest with you. 1,858 things saved since February 2015. 5.9 million words — roughly 79 books' worth. The page cheerfully informs me I've finished reading 1% of what I saved to read. I choose to interpret the other 99% as optimism about my future self.

The stats page showing collection totals, reading progress, and top themes

The backlog reality check. Eleven years of saving, 494 hours of unread reading. No regrets.

Chat With Your Library

The newest feature is the one I'm most excited about. I can now ask questions across the whole collection: "What advice on parenting comes up most often?" "What are the most consistent productivity ideas I've saved?" "What facts about Amazon have I collected?"

Under the hood this is an agentic loop. Claude gets three tools — search the library's full-text index, read a full item (including my highlights), and list the collection's tags — and it decides how to use them. Ask a thematic question and it runs four or five searches with different wordings, skims the previews, reads the strongest matches in full, and writes a synthesis where every claim links back to the actual saved item.

The chat view answering a question about parenting advice, showing its searches and a cited answer

Asking the library about parenting. The gray lines at the top are the searches and reads it chose to run; every citation links to the item in my library.

Watching it work is a little uncanny. It surfaced connections I'd forgotten — an essay on Quaker parenting and a cross-cultural NYT piece making the same argument about autonomy, saved eight years apart. A decade of curation turns out to be a pretty good dataset when something can actually read all of it.

Getting a Decade of Data In

The hardest part wasn't building the app — it was the archaeology. My history lived in a Pocket export and a Readwise Reader export, with overlapping content and inconsistent metadata. Import scripts mapped both into the same schema, then a series of backfill scripts filled the gaps: extracting article text for old saves, fetching metadata and thumbnails, running AI enrichment over everything, even pulling YouTube transcripts so videos are searchable too.

My favorite bug from this process: the two imports disagreed about URL schemes. Pocket had saved everything as http://, Readwise as https://, so the deduplication that keys on canonical URLs sailed right past 95 pairs of the same article. A merge script fixed it — keeping whichever copy had the richer content, combining the tags, and preserving the original save dates back to 2015.

Architecture Notes

A few choices I'd make again:

One process, no services. The background job queue is a SQLite table drained by an in-process worker inside the Next.js server. No Redis, no cron, no message broker. For a single-user app, "web scale" is one person clicking save a few times a day.

FTS5 with triggers. The search index is maintained by SQLite triggers, not application code. Insert or update an item and the index updates itself. Search across 5.9 million words returns in milliseconds on a $6 VM.

A type registry. Every item type — article, video, tweet, email, note, image, file — is defined in one registry file with its traits. The capture pipeline, the cards, the filters, and the AI classification all read from it, so adding a type is one entry plus whatever behavior is genuinely new.

AI as a layer, not a foundation. Every AI feature degrades gracefully. No API key? Capture, search, and reading all work fine — you just don't get summaries or chat. The app's core doesn't depend on anyone's model.

Never trust a URL. The app fetches user-supplied URLs from a server, which is a classic server-side request forgery setup. All outbound fetches go through a guard that resolves DNS and re-checks every redirect hop against private address ranges.

Deployment

The whole thing runs on the smallest DigitalOcean droplet — one shared CPU, 1GB of RAM, $6 a month — behind Caddy, which handles HTTPS automatically. Deployment is a Makefile target: push to git, pull on the droplet, build there so the native modules match the server's architecture, restart the systemd service. Total infrastructure: one VM, one domain, zero external services.

The 1GB of RAM occasionally keeps things interesting — the article-extraction library is memory-hungry enough that big backfills run on my laptop and ship the results up as SQL. But that constraint feels right for a personal tool. And because the database is one file, the backup strategy is make sync-data: an online SQLite backup pulled down to my laptop, which doubles as my local development database. Production data on my machine, real backups, dev environment — one command.

What I Learned

Owning your data changes how you use it. When the archive was in someone else's app, it was write-only — save and forget. Now that it's mine, in a schema I control, I keep finding new things to do with it: stats, highlights, transcripts, chat. The data didn't change. The ownership did.

SQLite is wildly underrated for personal software. Full-text search over millions of words, a job queue, and eleven years of data — one file, no server, backed up with cp. Most personal projects don't need more database than this.

Agentic search beats embeddings for this job. I expected to need a vector database for the chat feature. Instead, giving Claude a plain keyword search tool and letting it choose the queries works remarkably well — it searches the way a good librarian would, trying synonyms and following leads. Sometimes the boring index is enough.

A decade of curation is a gift to your future self. Every one of those 1,858 saves was a moment of "this matters to me." I couldn't have known I was building a dataset for an AI to read back to me — the tools didn't exist. Save things you care about. Storage is cheap, and future you will have better tools than present you can imagine.