Open-source project
learningequality/kolibri avatar
learningequality/kolibri

Kolibri: an offline-first learning platform built as a plugin ecosystem

Kolibri Learning Platform: the offline app for universal education

1,159 stars1,011 forksPythonMIT

At a glance

What is it?
Kolibri is Learning Equality's offline-first platform for teaching and learning without an internet connection, and this repository is the developer side of it: a Python backend and a JavaScript frontend, and a plugin architecture that makes most of the application's features installable entry points. It is an MIT-licensed project whose published releases are still in the 0.x line.
Who is it for?
Adopt Kolibri if you are deploying learning content to classrooms or communities with unreliable connectivity, and you want a platform whose content library, its peer-to-peer distribution model and its plugin system are already built. Do not adopt it if you need a hosted multi-tenant service or a stable release API, since the published line is 0.x with alpha builds of 0.20.0 and the default branch is develop.
Can I use it commercially?
Yes. MIT 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 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

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

Editorial analysis

What Kolibri is for, and who it is aimed at

Kolibri is described as the offline-first platform for teaching and learning with technology without requiring the Internet, and it was developed with and for the community by Learning Equality.

The offline-first framing is the whole point, and it is a different bet from building a web application that degrades gracefully when the network drops. Kolibri is designed for the case where the network is simply not there: a school in a place with intermittent connectivity, a learning centre where devices sync when they can, a community with a local server that holds the content and does not depend on a remote service being reachable.

The categories in the repository make the intended context clear. The topics are edtech, education, hacktoberfest, oer, offline-first and sdg-4. The OER tag points at the open educational resources ecosystem, where the constraint is that teaching materials are openly licensed and therefore a platform has to be able to ingest a wide range of third-party content rather than only its own. The sdg-4 tag points at Sustainable Development Goal 4, the United Nations goal on quality education, which is the frame Learning Equality works within.

The result is an application with a local-first architecture where the device holds the content, the content is served from the device, and the platform is designed so that a deployment does not need a permanent connection to anything.

The licensing is MIT and the primary language is Python, and the repository's own README is explicit about its audience in its first line: this repository is for software developers wishing to contribute to Kolibri, and if you are looking for help installing, configuring and using Kolibri you should refer to the User Guide. That division matters, and the article that follows is about the developer side of the project.

The plugin system, where most of the application actually lives

The pyproject.toml contains the clearest statement of the architecture in the whole repository, and it is the entry points table.

The console script is `kolibri = "kolibri.utils.cli:main"`, so the application is a command line program. But the interesting part is the group named kolibri.plugins, whose entries map module names to their own paths:

toml
[project.entry-points."kolibri.plugins"]
"kolibri.plugins.app" = "kolibri.plugins.app"
"kolibri.plugins.coach" = "kolibri.plugins.coach"
"kolibri.plugins.default_theme" = "kolibri.plugins.default_theme"
"kolibri.plugins.device" = "kolibri.plugins.device"
"kolibri.plugins.epub_viewer" = "kolibri.plugins.epub_viewer"
"kolibri.plugins.html5_viewer" = "kolibri.plugins.html5_viewer"

Each line registers a plugin by name. A plugin is a directory of Python that the application discovers at runtime through the packaging metadata rather than through an import list in the source. The names tell you what the plugin set contains: app, coach, default_theme, device, epub_viewer, html5_viewer, facility.

Those names describe the product. The app plugin is the main Kolibri application. The coach plugin is the role-based piece, the interface a coach uses to assign content to learners. The default_theme plugin carries the visual theme, which is why theming is a plugin rather than a setting, and why a deployment can rebrand the whole product. The device plugin is the content device, the side of the model that holds and syncs the content library. The epub_viewer and html5_viewer plugins are the two content renderers, and their existence as separate plugins is the interesting design decision: the same library holds both EPUB documents and web-format HTML5 packages, and the platform does not have to know in advance which is which.

The facility plugin is the one that deserves a note. In Kolibri's model, a facility is a local server that holds a content library and serves it to devices on the same network. That is the piece that makes the offline-first claim work, because the devices do not each hold a full copy of everything, and the facility is the thing that syncs with the wider world when it can.

The consequence for an adopter is that extending Kolibri means writing a plugin and registering an entry point, not patching the application. The consequence for an operator is that the set of installed plugins is part of the deployment, and a plugin built against a different Kolibri version may not load.

Two build systems and two test suites in one repository

The top-level listing shows a project that is two applications sharing a repository.

On the Python side there is the kolibri/ directory with the application, python_packages/ for the vendored or internal Python packages, pytest.ini for the test configuration, setup.py, pyproject.toml, a requirements/ directory, requirements.txt and a Makefile. On the JavaScript side there is packages/, which includes kolibri-common and kolibri as the search paths named in the build scripts, plus pnpm-workspace.yaml, pnpm-lock.yaml, package.json, jest.conf.js, babel.config.js, eslint.config.mjs,.prettierrc.js and.stylelintrc.js. There is also platforms/, which is where the per-platform packaging lives, and docker/ with a docker-compose.yml at the root.

The CI badges confirm the split. There is a separate Python test status badge pointing at a tox.yml workflow and a separate JS test status badge pointing at a frontend-tests.yml workflow, both against the develop branch. Two test runners, two configuration files, two lock files, and the CI treats them as separate jobs.

The JavaScript build is worth a closer look because it shows how the frontend is assembled. The build script runs `pnpm run sandbox-build` and then `kolibri-build prod --file./build_tools/build_plugins.txt --transpile --cache`, and the watch script runs `kolibri-build dev --file./build_plugins.txt --cache`. Both take a plugin file, which is the same plugin list as the Python entry points, expressed as a file the JavaScript build can read. So the plugin manifest is the shared contract: the Python side discovers plugins through packaging metadata, the JavaScript side discovers them through a text file, and both must agree about what is installed. A plugin with a Python backend and a JavaScript frontend has to be registered in both places.

The two lock files are also a signal. pnpm-lock.yaml governs the JavaScript dependency tree and uv.lock governs the Python one, so the project has moved to a modern Python lockfile rather than living only with requirements.txt.

The development server command shows how the pieces are wired together at runtime:

bash
PYTHONPATH=. KOLIBRI_PLUGIN_ENABLE=integration_testing.development_plugin kolibri start --debug --foreground --port=8000 --settings=kolibri.deployment.default.settings.dev

The PYTHONPATH override puts the checkout on the import path so the local code is used rather than an installed copy, KOLIBRI_PLUGIN_ENABLE turns on a plugin from the integration_testing/ directory, and the settings module selects a development deployment profile. The port is 8000, foreground keeps the process attached so you can see the log, and --debug turns on the development behaviour. This single command is the answer to how a developer runs the whole stack locally.

Translation, which is a first-class part of the build

The package.json contains four separate translation commands, and the number of them is itself the interesting fact.

makemessages runs `kolibri-i18n extract-messages` against two namespaces at once, kolibri-common with its search path at./packages/kolibri-common and kolibri-core with its search path at./packages/kolibri. createprofiles runs `kolibri-i18n profile` writing to./kolibri/locale/en/LC_MESSAGES/profiles/strings.csv. auditdittostrings runs `kolibri-i18n audit` producing a ditto.csv, and transfercontext runs `kolibri-i18n transfer-context`.

A platform that serves learning content in a community with no reliable internet is a platform that serves it in the community's language, so translation is not an afterthought here, it is a build step with its own tooling, its own command names, and its own output files tracked in the repository under kolibri/locale/en/LC_MESSAGES/. The four commands map onto a real localisation workflow: extract the strings from the source, generate a profile that describes the context of each string so translators can see where it appears, audit for strings that duplicate another string, and transfer context between versions so a translator's notes survive a release.

The kolibri-i18n tooling is a separate console program with subcommands, which is the shape of a dedicated localisation tool rather than a set of ad-hoc scripts. The crowdin.yml file at the repository root points at Crowdin as the translation platform, so the workflow is extract locally, then send the catalogue to a translation service, then bring the translations back.

For anyone evaluating the project, this is a good indicator of how it is built. The same is true of the other tooling in the tree:.pre-commit-config.yaml with a.pre-commit-hooks/ directory,.sqlfluff for the SQL linting,.deepsource.toml for static analysis,.editorconfig, and an AGENTS.md and a CLAUDE.md at the root for tooling that reads repository instructions.

Packaging: a setup.py shim, a requirements.txt sink, and python-for-android

Three files in this repository explain more about how Kolibri is built and shipped than any amount of prose would, and all three are unusually explicit about why they exist.

The setup.py is a thin compatibility shim:

python
#!/usr/bin/env python
"""
Thin compatibility shim for tools that expect setup.py (e.g. python-for-android).
All configuration is in pyproject.toml.
"""

from setuptools import setup

setup()

The real configuration is in pyproject.toml, and setup.py exists only because some tool still expects to find it. The tool named is python-for-android, which is how a Python application gets packaged into an Android application, and that is the reason the shim cannot simply be deleted. The project supports Android as a platform, and the platforms/ directory in the tree is where the platform-specific work lives.

The requirements.txt is equally explicit about being a placeholder rather than a source of truth:

code
# Bundled runtime dependencies are resolved from the `base` dependency group in
# pyproject.toml by `make staticdeps`. This file exists only as the sink for any
# EXTRA_REQUIREMENTS injected at build time by build_tools/customize_requirements.py.

So the bundled runtime dependencies live in a dependency group named base in pyproject.toml, and a make target called staticdeps resolves them. The requirements.txt file is where the build injects any extra requirements, which is how a downstream packager can add dependencies without forking the manifest.

The pyproject.toml itself then declares the project with no dependencies listed at all, with `dynamic = ["version"]` because the version is derived by setuptools-scm from the git tag rather than written in the file, and the build requirement pinning setuptools-scm at 9 or later. There is a comment in the build-system section explaining exactly why: setuptools-scm 9 added SETUPTOOLS_SCM_PRETEND_METADATA, which the Makefile relies on, and 8.x ignores it silently and stamps a dirty version. That is a precise, small trap documented in place.

The Python support window is stated as >=3.6 and <3.15, with classifiers for 3.6 through 3.14 and for PyPy, and the Development Status classifier says 4 - Beta. A distribution platform that has to run on cheap hardware in schools, and on old Android devices, has a reason to keep the floor low, and the upper bound means 3.15 is not yet supported.

The limits, and what the release line tells you

Three limits are worth naming, and none of them are defects.

The first is the release state. The recent releases are v0.19.5 on 2026-07-13, then v0.20.0-alpha0 on 2026-08-05 and v0.20.0-alpha0's successor v0.20.0-alpha1 on 2026-08-12. The default branch is develop, not main or master. So the stable line a deployment should use is the 0.19.x line, the 0.20.0 line exists only in alpha builds, and the branch you get by default is the development branch. The pyproject classifiers agree, calling the project 4 - Beta. For a platform that other organisations deploy and configure, that is a normal but real consideration: you are choosing a release that is young and a stable line that is older.

The second is the boundary of the content. Kolibri is a platform for content that has been imported into its library, and the library model is what makes offline work. That also means the platform is not a general-purpose content management system, and a deployment's first substantial task is getting the content in. The README for the developers says nothing about supported content formats beyond the presence of the epub_viewer and html5_viewer plugins, so the honest statement is that those two renderers are what the plugins directory names.

The third is the plugin contract. Because the plugin set is discovered at runtime and the JavaScript build reads a separate plugin file, a plugin has to be registered in two places, and a plugin written against one version of Kolibri may not load against another. There is no plugin API compatibility policy described in the repository, so a deployment that depends on a third-party plugin is carrying a dependency whose upgrade path is not documented here.

And there is a fourth, smaller one. The repository's README is short and developer-facing, and the user guide lives in a separate documentation site. Someone looking for installation instructions will not find them in this README, which points them to the user guide at kolibri.readthedocs.io, and the downloads are handled at learningequality.org/download.

Kolibri against the alternatives

The comparison that matters is with the other ways to get learning content to a place without reliable internet, and there are three distinct approaches.

The first is to package the content with the device. A USB stick of videos, a folder of PDFs, a set of downloaded files that travel with the laptop. The difference in approach is that Kolibri is a platform rather than a folder, so it tracks which content each learner has completed, it can assign content and let a coach see progress, and it can present HTML5 packages that a folder of files cannot run. What it costs is the platform itself: a Python runtime, a database, an update mechanism, and a local server, all of which the team deploying it has to keep working.

The second is to use a hosted learning management system and accept the connectivity requirement, or a hybrid where a local cache mirrors part of the content. The difference here is who owns the content and where the learner data goes. A hosted platform has a server the operator does not run, and the platform's operators can update it without the school's involvement. A local-first platform keeps the learner records and the content on the school's own hardware, and the operator owns the upgrade path.

The third is a different kind of tool: a media server or a file-sharing system that syncs content to devices over the local network, with no notion of learners, coaches, assignments or progress. That is simpler, and it is sufficient when the goal is to make a library of videos available. Kolibri's coach and device plugins are the parts that do the work the simpler option cannot, and they are also the parts that make it a larger system to deploy.

So the deciding question is whether you need to know what a learner did. If the answer is no, a local file server or a USB stick is cheaper and will not need a support contact. If the answer is yes, and the connectivity is unreliable enough that a hosted platform is not an option, then an offline-first platform with a device and a coach model is a different kind of investment, and Kolibri is a complete implementation of it rather than a starting point.

Editorial conclusion

Adopt Kolibri if you are deploying learning content to classrooms or communities with unreliable connectivity, and you want a platform whose content library, its peer-to-peer distribution model and its plugin system are already built. Do not adopt it if you need a hosted multi-tenant service or a stable release API, since the published line is 0.x with alpha builds of 0.20.0 and the default branch is develop. Verify first that the content you intend to serve can be imported into the platform's library format, that the devices you deploy to can run the packaged build, and that the plugin you need is published for the version you install rather than only on the develop branch.

Frequently asked questions

What is Kolibri and who builds it?

Kolibri is the offline-first platform for teaching and learning with technology without requiring the Internet, developed with and for the community by Learning Equality. It is MIT licensed, written primarily in Python, and the releases are in the 0.x line with the default branch named develop.

How is Kolibri's plugin system structured?

Plugins are registered as Python packaging entry points in the group named kolibri.plugins, so the application discovers them at runtime rather than through an import list. The registered names include app, coach, default_theme, device, epub_viewer, html5_viewer and facility, and the JavaScript build reads a matching plugin file.

How do I run the Kolibri development server locally?

The package.json defines a python-devserver script that runs `kolibri start --debug --foreground --port=8000 --settings=kolibri.deployment.default.settings.dev` with PYTHONPATH set to the checkout and KOLIBRI_PLUGIN_ENABLE pointing at the integration_testing development plugin, so the local source and a development plugin are both active.

How does Kolibri handle translations?

The build uses a dedicated kolibri-i18n tool with commands for extracting messages, creating string profiles, auditing duplicate strings and transferring context, run over the kolibri-common and kolibri-core namespaces, with crowdin.yml at the repository root for the translation platform.

Official sources

  1. learningequality/kolibri on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
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/learningequality-kolibri.svg)](https://hysenlabs.com/projects/learningequality-kolibri)

Was this useful?