Open-source project
Roznoshchik/Lurnby avatar
Roznoshchik/Lurnby

Lurnby: an active reading practice with spaced repetition on top

A tool for active reading and personal knowledge management

705 stars18 forksJavaScriptBSD-3-Clause

At a glance

What is it?
Lurnby is a self hosted application for building a personal reading habit. It takes web articles and epubs, lets you highlight and categorise them, and then drills you with active recall and spaced repetition. The backend is Flask with Postgres, Redis, and SQLAlchemy, the frontend is Preact built with Vite, and the article extraction leans on readability-lxml and a Node wrapper around Mozilla's Readability.
Who is it for?
Lurnby fits a reader who wants one place for the articles and books they read, and who wants what you read is turned into something you are actively asked about later rather than something you read once and forget. It does not fit someone who wants a hosted service, since the project ships as an application you run yourself, and it does not fit a reader with a small backlog, because the indexing and review machinery earns its keep over months of material.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 175 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The reading loop the project is built around

Lurnby is a tool for building a personal knowledge practice, and the project describes its purpose as helping you read and remember more. The inputs it works on are web articles and epubs, and the outputs are highlights, categories, and review sessions built on active recall and spaced repetition.

That is the whole idea, and it is worth noticing how small the scope is compared with the tools that try to solve the same problem. There is no inbox, no sharing, no team feature, and no attempt to be a general purpose notes application. The cycle is: take something you have read, mark the parts worth keeping, file them, and then be asked about them later.

The active recall part is the part that makes the highlights worth collecting. A highlight that is never asked about is a highlight that is never remembered, so the review tools exist to convert a passive archive into a set of questions. The spaced repetition side handles the scheduling, and the topics the repository carries name the field directly, with personal knowledge management, personal knowledge system, reading, and spaced repetition system.

The project is BSD 3 clause licensed, the site is lurnby.com, and the last push was on 2026-05-01 with no tagged releases, so there is no version to pin and the repository is the release. The screenshots in the project documentation show a mobile view and a desktop view, which tells you the same interface is expected to work on a phone and on a laptop, and that is the shape of the tool: it is meant to be used in the gaps of a day rather than only at a desk.

The contributing section is short and conventional. Features and issues live in the issue tracker, and the instruction to fork the repository and submit a pull request for review is the whole process.

Flask, Postgres, Redis, and the pieces around them

The backend is a Python application, and the badge row in the project documentation names the four things that matter: Python, Flask, Postgres, and Redis, with SQLAlchemy in the middle for the data access.

The pinned requirements show what that means in practice. Flask is at 3.1.2 and arrives with the extension set that a real application needs rather than a toy one: CORS handling, HTTP authentication, login management, mail sending, migrations, an ORM integration, a security headers library, and form validation with WTForms behind it. A gunicorn dependency at 23.0.0 is what serves it in production, and the Dockerfile installs it separately from the requirements file, which is a sign that it is treated as a deployment concern.

The database is Postgres, through psycopg2 at 2.9.11, and the schema is managed by Alembic, at 1.16.5, with Flask-Migrate providing the integration that ties the two together. SQLAlchemy itself is pinned at 2.0.45 with the companion utilities package, so the data layer is a standard SQLAlchemy setup rather than anything custom.

Redis appears twice in the requirements, once as the client library and once as the queue library. The queue library, rq, is the interesting one, because it is what lets a piece of work happen outside the request. Extracting the text of an article, or generating something from it, is slow enough that doing it inline in a web request is a bad idea, and a Redis backed queue is the conventional way to avoid it.

The rest of the list is the accumulated surface of a project that grew over time, and reading it tells you what the application actually does. EbookLib is there for epubs. PyMuPDF is there for PDF files, and the container installs the command line tools for mupdf and pdftk as well. A readability library and html5lib and BeautifulSoup and lxml are there for articles. PyJWT is there for tokens, boto3 for object storage, pyngrok for exposing the local instance through a tunnel, python-dotenv for the environment, and Faker for filling the database with test data. There is also a dependency tree snapshot file, which is the kind of thing a project keeps to notice when a transitive dependency changes.

A Preact frontend with components and services documented separately

The frontend is a separate part of the repository, in a client directory, and the badges name the stack: Preact for the components, Vite for the build, JavaScript as the language, SASS for the styles, HTML5 for the markup, and Jinja for the templates.

That combination is worth pausing on, because Jinja on the server and Preact in the browser is a mixed model. It means the pages are rendered on the server with templates, and the interactive parts are small islands of Preact mounted into them, which is a different approach from a single page application where the browser renders everything.

The repository documents the frontend in two separate documents, and the split tells you how the code is organised. One is a list of reusable components, and the other is a list of frontend services. Components are the things a page is built from, and services are the things that talk to the backend, so the boundary is between the view layer and the data layer, with the services owning the requests and the components owning the rendering.

The frontend is JavaScript rather than TypeScript, which is visible in the package manifest, and the build tool is Vite, so the dev server and the bundler are the modern ones.

The npm manifest at the root of the repository is not the application's manifest. It describes a package called ReadabiliPy at version 0.1.0, described as an augmented Python wrapper for the Mozilla standalone Readability.js package, with the main entry point pointing at an extraction script, the Apache 2.0 licence, a requirement of Node 11 or newer, and two dependencies: a DOM implementation and an argument parser.

So the Node toolchain in this project exists for one specific job, and it is the job of turning a web page into clean article text. The Python side calls it, and the augmented part means the wrapper adds behaviour to the standalone extraction script rather than using it as published.

Getting an article or a book into the system

The extraction path is the part of the project that has to deal with the messiness of the web, and the requirements show two different strategies for it.

For a web article, there is a pure Python route and a Node route. The pure Python one is a readability implementation built on lxml, with html5lib and BeautifulSoup alongside it for parsing. The Node route is the ReadabiliPy package, which wraps the standalone Readability script from Mozilla in a headless DOM provided by jsdom, and the container installs Node and npm precisely so that this path is available.

Having both is not redundancy for its own sake. The Python implementation is faster and has no runtime dependency, and the JavaScript one follows the reference implementation more closely, so a page that the Python parser mangles may still come out correctly through the Node one. Which one runs is a decision the project makes somewhere in the application code, and the presence of both suggests it falls back.

For epubs, EbookLib does the work, and for PDFs there is PyMuPDF, with the container also installing the mupdf and pdftk command line tools so that a document can be processed outside Python when that is easier.

Everything that arrives goes through the same pipeline afterwards: the text is stored, you highlight and categorise it, and the review tools come back to it later. The interesting design decision is that the extraction is not a separate feature but the front door of the reading loop, and the fact that the project ships a Node dependency for it says how much of the modern web the parser has to survive.

Deployment surfaces, and why there are several

The repository carries several ways to run the application, and the choice of names tells you what they are for.

There is a Dockerfile for containers, a Procfile for a platform that expects a process list, a template and a configuration file for the AWS serverless tooling, a justfile as a task runner, a boot script, and a WSGI module. There is also a docker run document, a macOS install note, an environment example file, and a Flask environment file.

The container image is the most instructive of the set. It starts from Python 3.12, installs the pieces a document pipeline needs at the system level, including the Postgres client headers, a compiler, the Node toolchain, and the mupdf and pdftk command line tools, and then it sets up the Node source repository before installing Node again, which is a step that exists to get a current version rather than the distribution's.

The application itself is installed into a virtual environment in the image, and the file that the container copies in is short and specific: the application package, the migrations, the Flask entry point, the configuration module, the boot script, the data module, and the two npm files. The boot script is then made executable and set as the entry point, and the Flask environment variable points at the entry module so that the WSGI server knows what to load.

The container then creates an unprivileged user, gives it the working directory, switches to it, exposes port 5000, and runs as that user. The non-root user is the part that matters for anyone deploying this, because the process that parses files from the internet should not be running as root inside a container.

The serverless template and its configuration file are the other option, and the presence of a directory named for the build output in the formatting configuration suggests that the same application is also built for that path.

Formatting, tests, and where the project keeps its rules

The tooling configuration is small but it is the interesting part of the repository, because the rules tell you what the maintainers care about.

The project configuration file configures one tool, the formatter, and does so with a line length of 119 rather than the more common 88 or 100. The exclusion list is more revealing: the source control directory, the Python cache, the GitHub directory, the serverless build output, the editor configuration, a distribution directory, the frontend directory, the pytest cache, the virtual environment directory, the ReadabiliPy application directory, and the migrations directory.

Two of those exclusions are deliberate decisions about what the formatter may touch. The frontend is excluded because the styles and the JavaScript are not Python, and the migrations are excluded because generated migration files are usually left exactly as the tool wrote them, so that a reformat does not produce a diff in a file that is supposed to change only when a model changes.

The rest of the tooling is spread across the repository rather than in one place. There is a flake8 configuration and a pre-commit configuration, a mypy extensions dependency, a pytest and a coverage dependency, and a coverage directory in the tree, which suggests the coverage output is written next to the code rather than ignored. The project also has a requirements file and a tree backup of it, a lock file for the npm side, and a justfile for the commands a developer runs.

The remaining top level files are the ones a Flask project accumulates: the configuration module, the data module, the entry point, the WSGI module, the migrations directory, the tests directory, the install document, and a small directory whose name is not explained by the reading available here.

Editorial conclusion

Lurnby fits a reader who wants one place for the articles and books they read, and who wants what you read is turned into something you are actively asked about later rather than something you read once and forget. It does not fit someone who wants a hosted service, since the project ships as an application you run yourself, and it does not fit a reader with a small backlog, because the indexing and review machinery earns its keep over months of material. Verify first that the runtime Python version and the database match the versions the project pins, that the Node dependency for article extraction is available in your environment, and that you read the install document rather than the container recipe, since the project documents several ways in and they do not all produce the same setup.

Frequently asked questions

What does Lurnby do?

It is a tool for building a personal knowledge practice. It works with web articles and epubs, lets you highlight and categorise them, and then reviews that material with active recall and spaced repetition, so that what you read is asked back to you later.

What is the stack behind Lurnby?

The backend is Flask with SQLAlchemy, Postgres through psycopg2, and Redis, which is also the queue behind the background job library rq. The frontend is Preact built with Vite, styled with SASS and templated with Jinja, and gunicorn serves the application in production.

How does Lurnby extract the text of a web article?

There are two routes. One is a pure Python readability implementation built on lxml, with html5lib and BeautifulSoup for the parsing. The other is a Node package, ReadabiliPy, described as an augmented Python wrapper for the Mozilla standalone Readability.js package, which runs the reference implementation against a headless DOM.

How do I run Lurnby?

The project documents the setup in an install document, and the repository also carries a Dockerfile, a Procfile, an AWS serverless template with its configuration, a justfile, and separate notes for running it in a container and on macOS. The container builds from Python 3.12, creates an unprivileged user, exposes port 5000, and starts the boot script.

What does Lurnby do with epubs and PDFs?

Epub files are handled with EbookLib, and PDF files with PyMuPDF. The container also installs the mupdf and pdftk command line tools, so that a document can be processed outside the Python process when that is the simpler route.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Roznoshchik/Lurnby on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/roznoshchik-lurnby.svg)](https://hysenlabs.com/projects/roznoshchik-lurnby)

Was this useful?