Build it yourself

intermediate one sitting reading

Build your own reading archive (a personal Readwise)

You will build a local reading archive that imports highlights from files you already have, reviews them with spaced repetition, searches the whole collection instantly, and exports everything as Markdown, CSV, and JSON. The core loop fits in a focused sitting. Readwise still charges monthly because capture works across dozens of apps, reading state syncs between devices, and its parsers get repaired whenever publishers change their sites.

What you'll learn

  • Parsing messy real-world export files, JSON, CSV, and Kindle clippings text, into a typed SQLite schema
  • Implementing a simplified SM-2 spaced repetition scheduler with due-date queries
  • Full-text search over SQLite using FTS5
  • Writing a bookmarklet that talks to your own validated, rate-limited API
  • Designing lossless exports in Markdown, CSV, and JSON that double as backups

Before you start

  • Node.js 22 installed, confirmed by running node -v
  • A desktop browser such as Firefox or Chrome for the bookmarklet step
  • At least one real highlight export, such as a Kindle My Clippings.txt file or any JSON or CSV file of quotes
  • A terminal, a code editor, and comfort running npm commands

The build

DELEGATE

Create the repository skeleton with Next.js 15, TypeScript, SQLite through Drizzle ORM, and one setup command that leaves you with a running home page. Drizzle is a thin library that maps TypeScript objects onto SQL tables, so the schema file is plain code you can read. Ask for the diff and study the highlights table it defines, because every later step builds on it.

step prompt
Build the starter for a local reading archive web app. Requirements:
- Next.js 15 with TypeScript and the App Router, plain CSS modules, no UI kit.
- SQLite stored at data/archive.db using Drizzle ORM, migrations generated with drizzle-kit.
- Schema in src/db/schema.ts defining a highlights table with id, text, author, title, source_file, location, url, tags_json, captured_at, times_seen, next_review_at.
- Home route at / showing row counts read from the highlights table.
- DATABASE_PATH read from .env, ship .env.example, never commit credentials.
- One command, npm run setup, that creates the database, applies migrations, and prints next steps.
- Out of scope: login screens, dark mode, component libraries, Docker.
- Expect drizzle-kit config path trouble, verify the migrations folder exists before moving on.
WE

Wire the importer to the highlights table from step 1 and feed it real exports. Start with the two fixture files, then run it against a genuine Kindle clippings dump if you have one, because real files carry encoding surprises and repeated page headers. Watch the duplicate counter, hashing text plus author is what keeps the archive clean over months of imports.

step prompt
Add a highlight importer that reads user-exported files into the highlights table in src/db/schema.ts. Requirements:
- POST /api/import accepts multipart uploads of .json, .csv, and .txt files up to 10 MB.
- Parse three formats: JSON arrays of highlight objects, CSVs with text and author columns, and Kindle My Clippings.txt blocks separated by ========== lines.
- Skip duplicates by inserting only rows whose sha256 hash of text plus author is new.
- Page at /import shows an upload form and reports rows imported and duplicates skipped per run.
- Create fixtures/sample-clippings.txt with 5 clippings and fixtures/sample-export.json with 10 highlights, and a test asserting the combined run inserts 15 rows.
- Strip stray page headers and fix encoding artifacts defensively, clippings files are always messy.
- Out of scope: calling Kindle or Readwise APIs directly, OCR from screenshots.
WE

Turn the archive into a study tool. Spaced repetition shows each highlight right before you would forget it, and a simplified SM-2 scheduler with 1, 3, and 7 day intervals is enough to feel the effect within a week. Search uses SQLite FTS5, a built-in full-text index that stays fast as the collection grows into the thousands.

step prompt
Add tagging, full-text search, and the review queue on top of the importer from step 2. Requirements:
- Full-text search over text and author using SQLite FTS5, exposed at /search?q= with highlighted matches.
- Inline tag editing per highlight, persisted to the tags_json column in src/db/schema.ts.
- Page at /review serving up to 10 due highlights per day, ordered by next_review_at ascending.
- Four grading buttons, Again, Hard, Good, Easy, updating next_review_at with simplified SM-2 intervals of 1, 3, 7, and 14 days.
- Header counter on /review showing how many highlights are due today and how many were reviewed this week.
- Keep search server-side with FTS5 match queries, do not load the whole table into the client.
- Out of scope: Anki sync, configurable algorithm settings screens, streak gamification.
WE

Give the archive a way to grow between imports. A bookmarklet is a tiny script saved as a browser bookmark that runs on whatever page you are viewing and sends the quote to your own API. Keep capture humble, store the link and the quote, and leave automatic page fetching alone since paywalls and bot defenses make that a swamp.

step prompt
Add capture paths that insert new rows into the highlights table built in step 1. Requirements:
- POST /api/capture accepts JSON with url, title, selection, and tags, validated with zod, inserting with source_file set to bookmarklet.
- Page at /capture renders the bookmarklet snippet as a draggable link that posts the current tab's url, title, and selected text.
- A manual form on /capture for pasting a quote, author, and URL when the bookmarklet misbehaves.
- Rate limit /api/capture to 30 requests per minute per IP to stop runaway loops.
- Store the URL string only, do not fetch page content, paywalls and bot defenses make scraping unreliable and off limits.
- Return clear JSON errors with status codes so the bookmarklet can surface failures.
- Out of scope: browser extensions, mobile share sheets, screenshot capture.
BY HAND

Open /capture in your browser and drag the bookmarklet button onto the bookmarks bar, something no script can do for you. Visit five pages worth quoting and click the button on each, then paste one quote through the manual form as a fallback drill. Finish by confirming six new rows now exist in the highlights table.

WE

Make leaving easy and staying a choice. Exports in Markdown, CSV, and JSON prove the data stays portable, and the JSON backup doubles as your disaster recovery story. Run the export twice, once to read the files and once to restore into a scratch database, because an untested backup is a hope, not a backup.

step prompt
Add exports that cover every column of the highlights table populated by steps 2 through 4. Requirements:
- Write Markdown files to exports/markdown/, grouped into one file per title, filenames slugged from the title with author and date in frontmatter.
- Write exports/backup.json containing every row and column of the highlights table.
- GET /api/export/csv streams CSV with exactly the columns the step 2 importer parses, so imports and exports round trip.
- Page at /export offers three buttons and reports the file count written after each run.
- Slug titles safely, reject path traversal characters, and overwrite cleanly on repeat exports.
- Verify the JSON round trip: restoring exports/backup.json into a fresh database yields identical row counts.
- Out of scope: Notion or Obsidian API sync, scheduled cloud backups.

What you won't get

  • Your archive covers files you import and quotes you capture by hand, with exports in open formats
  • Pages behind logins or paywalls are stored as links and quotes only, respecting publisher terms
  • Capture happens from your desktop browser through the bookmarklet and forms
  • Review order comes from your own schedule rather than recommendations drawn from other readers
  • Feeds are ones you supply yourself, the app stores links and quotes rather than licensed article text

Why people still pay — and what that teaches you

integrations: Readwise wins by pulling highlights from dozens of sources automatically, and each connector is ongoing maintenance against someone else's changing export formats. A builder learns to start with file imports, which never break silently, and to add live connectors only when the pain of exporting by hand becomes real.

execution-polish: The daily review feels effortless and the archive stays clean without parser repairs, and that reliability is what people renew. A builder learns that the review loop you actually open every morning beats a longer feature list, so spend the saved time on deduplication and empty states.

content-rights: Readwise's Reader serves licensed full-text articles through publisher agreements, which costs money and relationships no solo project can replicate. A builder learns to stay inside open content, storing links and your own quotes, which keeps the project legal and maintenance-free.

Stretch goals

  • Subscribe to a handful of RSS or Atom feeds you pick yourself and import quotes from entries you flag
  • Support OPML import and export so your subscription list travels between readers
  • Add a weekly recap page that resurfaces five random highlights from the archive

About Readwise

Readwise costs $9.99/month. People still pay for Readwise because people pay when capture works everywhere, reading state syncs instantly, and the archive remains clean without constant parser repairs. The recurring cost buys feed quirks, parsing, paywalls, canonical URLs, deduplication, images, search, sync, browser extensions, and publisher changes, not just the visible interface.

Sources & further reading

  • Readwise — Try the demo review flow to see the daily loop you are rebuilding
  • Readwise pricing — Current tiers, useful for judging which parts of the scope are worth paying for
  • FreshRSS — Mature self-hosted reader, a reference implementation for polite feed handling
  • Anki — See where spaced repetition goes when you want scheduling far deeper than SM-2 lite

Keep building

New lessons and honest build notes, by email. No spam, one-click out.

Signups open when the site goes live.