Home / Docs
TutorLoom documentation
Everything you need to install, configure, run and extend the open-source Community edition. Enterprise customers get the same platform, set up and supported by our team.
On this page
Introduction#
TutorLoom is a self-hosted, multi-tenant learning management system for companies, colleges and training institutes. One installation runs training for many organisations. Each organisation onboards its trainers and learners, groups them into batches, builds courses with videos, documents, tasks and quizzes, and tracks every learner’s progress.
The repository holds three applications:
- Backend API: Python 3.13, Django 6 and Django REST Framework, at the repository root.
- Web app: React 19, TypeScript and Vite, in
frontend/. - Learner mobile app: Flutter for Android and iOS, in
student_mobile_app/.
At a glance (v1.0.0)
| Features | 117 across the Community and Enterprise editions |
| Portals | 4 behind one login: Super Admin, Organisation Admin, Trainer and Learner (see Roles and portals) |
| Coding judge | 7 languages, in the Enterprise edition |
| Automated tests | 396 backend, 945 web app and 342 mobile app (see Running tests) |
Quickstart with Docker#
The recommended way to try it. Docker runs Postgres, Redis, the API, a Celery worker and the web app together. You need nothing else installed.
git clone https://github.com/pacewisdomhq/TutorLoom.git cd TutorLoom docker compose up --build
| Service | Address |
|---|---|
| Web app | http://localhost:5173 |
| API docs (Swagger) | http://localhost:8010/api/docs/ |
| ReDoc | http://localhost:8010/api/redoc/ |
| Django admin | http://localhost:8010/admin/ |
Your source folders are mounted into the containers, so edits reload without a rebuild unless dependencies change. Database migrations run automatically every time the backend starts.
Ports
The API is published on 8010 because 8000 is often taken. Postgres and Redis are not published, so they never clash with ones you already run. Change host ports in a .env file next to docker-compose.yml, or inline:
BACKEND_PORT=9000 FRONTEND_PORT=3000 docker compose up
The web app’s VITE_API_BASE_URL follows BACKEND_PORT automatically.
Everyday commands
docker compose logs -f backend # follow a service's logs docker compose exec backend bash # open a shell in the API container docker compose exec backend python manage.py migrate docker compose down # stop; add -v to also delete the database
Your first organisation#
Create an admin user, then an organisation with that user as its admin.
- Create the user
bash
docker compose exec -e DJANGO_SUPERUSER_PASSWORD='SecurePassword123' backend \ python manage.py createsuperuser --noinput \ --email [email protected] --first_name Acme --last_name Admin
- Create the organisation and its first batch
bash
docker compose exec backend python manage.py create_initial_organization \ --name "Acme Academy" --slug "acme-academy" \ --email "[email protected]" --admin-email "[email protected]" --batch-name "Batch 1"
- Sign in
Open
http://localhost:5173and sign in with the email and password from step 1.
create_initial_organization links an existing user as the organisation admin. It can’t create the user, so run step 1 first. Other options: --slug, --email (organisation contact) and --batch-name (default "Batch 1").Manual setup without Docker#
For development on your own machine. You need Python 3.13, Poetry 2.x, PostgreSQL, Redis, and Node.js with npm.
1. Install
git clone https://github.com/pacewisdomhq/TutorLoom.git
cd TutorLoom
poetry env use python3.13
poetry install
poetry run pip install Pillow # needed for image fields, not yet in pyproject.tomlPrefer pip? python3.13 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
2. Configure
cp .env.example .env
At minimum set SECRET_KEY, the DB_* variables (or DATABASE_URL), REDIS_URL and CELERY_BROKER_URL. See Configuration for every variable.
3. Database and first organisation
poetry run python manage.py migrate poetry run python manage.py createsuperuser # you sign in with your email address poetry run python manage.py create_initial_organization \ --name "Acme Academy" \ --admin-email [email protected] # an existing user, who becomes the organisation admin
4. Run the backend
redis-server # or: docker run -d -p 6379:6379 redis poetry run python manage.py runserver poetry run celery -A lms_core worker -l info # background tasks
5. Run the web app
cd frontend npm install cp .env.example .env # set VITE_API_BASE_URL, e.g. http://localhost:8000/api npm run dev # http://localhost:5173
Configuration#
The backend reads its settings from environment variables, usually through a .env file at the repository root.
| Variable | What it does | Example |
|---|
Web app
Variables for the web app must start with VITE_, otherwise Vite leaves them out of the browser bundle.
| Variable | What it does | Example |
|---|---|---|
VITE_API_BASE_URLRequired | The backend API root the web app calls | http://localhost:8000/api |
VITE_CSRFTOKEN | Optional static CSRF token for non-browser or development setups | (empty) |
Roles and portals#
Each role has its own portal. One account can hold several roles and switch between them from the account menu without signing out.
| Role | What they do | Role in code | Edition |
|---|---|---|---|
| Super Admin | Runs every organisation in the group and every user | Django superuser | Enterprise |
| Organisation Admin | Runs one organisation: people, batches, courses and progress | org_admin | Community |
| Trainer | Delivers assigned courses, grades work, tracks learners | teacher | Community |
| Learner | Follows course roadmaps, submits work, tracks progress | student | Community |
| Job candidate | Takes a coding interview through a private link, no account needed | (none) | Enterprise |
Organisations and data separation#
A person has one login for the whole platform. Their membership in each organisation decides what they can see there.
- One identity. A user has a single set of credentials across the platform.
- Membership per organisation.
OrganizationMemberlinks a user to an organisation with a role:org_admin,teacherorstudent. - Scoped data. Courses, submissions and analytics queries filter on
organization_id, so one organisation’s data stays out of another’s.
Courses and progress#
A course is made of chapters (modules), and each chapter is made of steps (nodes).
- An admin or trainer creates a course and adds chapters and steps.
- Each step holds content: a video, document or link, a task to submit, or a quiz.
- The course is assigned to a batch.
- Learners in the batch open the course between the batch’s start and end dates.
- As learners complete steps,
StudentNodeProgressrecords it. Completion percentages update live on trainer dashboards and in Excel exports.
Editions#
These docs cover the open-source Community edition. Enterprise builds on the same codebase.
| Capability | Community | Enterprise |
|---|---|---|
| Organisation Admin, Trainer and Learner portals | Yes | Yes |
| Course builder, prerequisites, quizzes, tasks | Yes | Yes |
| Bulk upload, grading, progress tracking, points | Yes | Yes |
| Super Admin console across offices or campuses | No | Yes |
| Assessments with a coding judge, certificates | No | Yes |
| No-login coding interviews | No | Yes |
| Google and Microsoft sign-in, audit logs, branding | No | Yes |
See the full comparison or talk to sales about Enterprise.
Learner mobile app#
A Flutter app for Android and iOS, in student_mobile_app/. It is for learners: trainers and admins use the web app.
A learner who belongs to several organisations picks one after signing in. Courses, progress and certificates are then scoped to it, and the app applies that organisation’s name, logo and colours.
| Needs | Version |
|---|---|
| Flutter | 3.47 stable |
| Dart SDK | ^3.13.0 |
| Android | Android Studio and an SDK |
| iOS | Xcode and CocoaPods on a Mac |
cd student_mobile_app
cp .env.example .env # then set API_BASE_URL, no trailing slash
flutter pub get
flutter runTo point a build at another backend without editing .env, use --dart-define. Release and CI builds should do this:
flutter run --dart-define=API_BASE_URL=https://lms.example.com
Web app#
React 19, TypeScript and Vite, in frontend/.
npm run dev # development server npm run build # type-check and production build npm run lint # ESLint npm run test # Vitest, single run npm run test:coverage # Vitest with coverage npm run preview # preview a production build
npm run build runs a full TypeScript check before bundling, and it must pass before any pull request.Running tests#
Backend tests need a running PostgreSQL. Redis, S3 and Celery are stubbed automatically.
poetry run pytest -p no:cacheprovider --no-cov -q # fast run poetry run pytest # with coverage poetry run pytest curriculum/ --no-cov -q # one app
cd frontend npm test npm run lint
cd student_mobile_app flutter test
At v1.0.0 the backend has 396 tests, the web app 945 and the mobile app 342.
Deploying to production#
docker build -t lms-backend . docker run -d -p 8000:8000 --env-file .env lms-backend
Before you go live
- The image starts Django’s development server. In production run
gunicorn lms_core.wsgi:application --bind 0.0.0.0:8000behind a proxy that terminates TLS. gunicorn is already installed. - Set
DEBUG=False, a strongSECRET_KEY, and explicitALLOWED_HOSTS,FRONTEND_URLandCSRF_TRUSTED_ORIGINS. - Use
USE_S3=Truefor uploads, and SES credentials for email. - Run a Celery worker next to the web process.
- Deploy the web app separately as a static build:
npm run buildinfrontend/.
analytics.tasks.calculate_daily_analytics has no beat schedule in the project, so dashboards that rely on daily metrics stay empty until you schedule it.SECRET_KEY, DEBUG=True, ALLOWED_HOSTS=* and Django’s runserver.API overview#
Interactive docs live at /api/docs/ (Swagger) and /api/redoc/, with the OpenAPI schema at /api/schema/.
Every endpoint needs Authorization: Bearer <token>, except login, invites and password reset. Lists return 20 items per page. To try requests in Swagger, call POST /api/auth/login/, click Authorize and paste the access token.
| Area | Endpoints |
|---|---|
| Auth | POST /api/auth/login/ · refresh/ · logout/ · forgot-password/ · reset-password/ · accept-invite/ · GET verify-invite/ |
| Profile | GET/PATCH /api/users/me/ · POST /api/users/<id>/reinvite/ |
| Roles | /api/roles/ · /api/user-roles/ · update-role/ · delete-role/ |
| Organisation | /api/organizations/<org>/ · members/ · staff/ · batches/ · batches/<b>/students/ · students/ · …/bulk-upload-file/ · …/download-template/ |
| Dashboards | …/<org>/analytics/overview/ · …/batches/<b>/analytics/ · …/students/me/dashboard/ · …/teacher-dashboard/<teacher>/ · …/batches-overview/ |
| Course builder | /api/organizations/<org>/courses/ → modules/ → nodes/ · nodes/<n>/content/ |
| Learning | …/courses/<c>/roadmap/ · …/my-courses/ · …/my-progress/ · POST /api/nodes/<n>/complete/ |
| Submissions | /api/nodes/<n>/submit/ · task/submit/ · task/result/ · task/all-submissions/ · /api/quizzes/<q>/submit/ |
| Grading and progress | …/pending-evaluations/ · …/submissions/tasks/<s>/review/ · …/learner-progress/ · …/learner-progress/export/ · …/node-submission-details/ |
curl -s -X POST http://localhost:8010/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "SecurePassword123"}'
# then send the access token from the response
curl -s http://localhost:8010/api/users/me/ -H "Authorization: Bearer <access token>"Project structure#
lms_core/ # settings, root URLs, Celery app, email and storage utilities accounts/ # users (email login), JWT auth, invites, password reset, profile rbac/ # roles, role assignments, permission classes organizations/ # organisations, staff, batches, students, bulk upload curriculum/ # courses, chapters, steps, resources, tasks, quizzes, progress analytics/ # admin, trainer and learner dashboards, daily metrics gamification/ # points and levels settings/ # shared Amazon SES client frontend/ # React web app student_mobile_app/ # Flutter learner app docker-compose.yml # full local stack
Each backend app has its own README.md with deeper notes.
Tech stack#
| Area | Technology |
|---|---|
| Backend | Python 3.13, Django 6, Django REST Framework 3.16 |
| Auth | JWT via djangorestframework-simplejwt, with rotating refresh tokens and a blacklist |
| Database | PostgreSQL |
| Cache and queue | Redis, Celery, django-celery-results |
| Storage | Local filesystem or Amazon S3 |
| Amazon SES, or any Django mail backend | |
| Spreadsheets | pandas, openpyxl, xlrd for bulk uploads and exports |
| API docs | OpenAPI 3 via drf-spectacular |
| Web app | React 19, TypeScript, Vite, React Router |
| Mobile app | Flutter 3.47 |
| Tests | pytest, pytest-django, pytest-cov, Vitest |
Contributing#
Contributions are welcome. Read CONTRIBUTING.md and the Code of Conduct first.
- Fork and branch
Fork the repository and create a feature branch.
- Keep code where it belongs
Put code in the app that owns the feature, and scope every query to the organisation.
- Commit migrations with model changes
The test suite runs
makemigrations --check. - Add tests
Backend tests go in
<app>/tests/; web app tests sit next to the component. Make surepoetry run pytestandnpm testpass. - Open a pull request
Describe the change. Follow PEP 8 and match the style around your code.
Security#
Please don’t report vulnerabilities in public issues, discussions or pull requests. Use GitHub’s private vulnerability reporting and include the affected component, steps to reproduce, and the impact you expect.
We aim to acknowledge reports within 3 working days, keep you updated while we fix the issue, and credit you in the advisory unless you prefer to stay anonymous. Fixes land on main.
Licence#
The Community edition is licensed under the Apache License 2.0. Copyright 2026 Pace Wisdom Solutions Pvt. Ltd. See NOTICE for attribution.