Skip to main content
Kolibri Studio uses Django for the backend and Vue.js for the frontend, with a sophisticated sync mechanism for offline-first editing.

Tech Stack

Backend

  • Django 1.11
  • Django REST Framework
  • PostgreSQL
  • Celery for async tasks

Frontend

  • Vue.js 2.7
  • Vuex for state management
  • Vue Router
  • IndexedDB with Dexie.js

Frontend Architecture

Multiple Single-Page Applications

Studio is divided into multiple SPAs, each with a distinct purpose:
SPAPurpose
Channel ListDisplay and manage channels (public, editable), create channels and collections
Channel EditEdit channel contents and structure - the primary Studio interface
AccountsUser registration, login, password recovery
AdministrationStaff-only admin panel for user and channel management
SettingsUser account settings

Frontend Code Structure

All frontend code lives under contentcuration/contentcuration/frontend/:
frontend/
├── channelList/          # Channel listing SPA
├── channelEdit/          # Channel editor SPA (main interface)
├── accounts/             # Authentication SPA
├── administration/       # Admin SPA
├── settings/             # Settings SPA
└── shared/               # Shared code across SPAs
    ├── data/             # Data resources and IndexedDB
    ├── vuex/             # Shared Vuex modules
    ├── components/       # Shared components
    └── utils/            # Utilities

SPA Code Conventions

Each SPA follows these conventions:
<spaName>/
├── index.js              # Vue app initialization
├── router.js             # Vue Router routes
├── store.js              # Vuex store initialization
├── pages/                # Route components
├── components/           # Shared components within SPA
├── vuex/                 # Vuex modules
└── __tests__/            # Jest tests
Use the Webpack alias shared/... to import shared code across SPAs.

Backend Architecture

Django Structure

The backend uses Django with custom viewsets and serializers:
contentcuration/contentcuration/
├── models.py             # Django models
├── viewsets/             # DRF viewsets
│   ├── base.py           # Base viewset classes
│   ├── channel.py        # Channel viewsets
│   ├── contentnode.py    # Content node viewsets
│   ├── assessmentitem.py # Assessment viewsets
│   └── sync/             # Sync endpoint
└── tests/                # Python tests

Custom Base Classes

Studio uses custom DRF base classes for performance:
Located in viewsets/base.py, this viewset optimizes read performance by:
  • Using .values() queries instead of serializer reads
  • Defining a values tuple for fields to return
  • Using field_map to rename/transform fields
  • Supporting custom methods: get_queryset, prefetch_queryset, annotate_queryset, consolidate
Example:
class ContentNodeViewset(ValuesViewset):
    values = ('id', 'title', 'description', 'kind_id')
    field_map = {
        'kind': 'kind_id',  # Rename kind_id to kind
    }
Located in viewsets/base.py, this serializer enables bulk operations:
  • bulk_create - Create multiple instances
  • bulk_update - Update multiple instances
  • bulk_delete - Delete multiple instances
  • Tracks changes via self.changes list
Example:
class ChannelSerializer(BulkModelSerializer):
    class Meta:
        model = Channel
        list_serializer_class = BulkListSerializer

Data Flow

Read Flow

1

Frontend requests data

Vue component or Vuex action requests data via Resource API:
import { Channel } from 'shared/data/resources';

const channels = await Channel.where({ public: true });
2

Check IndexedDB cache

Resource API checks IndexedDB for cached data. If found and fresh (< 5 seconds old), returns immediately.
3

Fetch from backend

If stale or missing, fetch from Django REST endpoint:
GET /api/channels/?public=true
4

Store in IndexedDB

Response is stored in IndexedDB and returned to caller.
5

Update Vuex store

Dexie Observable triggers Vuex mutations via listeners to update UI state.

Write Flow

1

User edits data

User makes changes in the UI (e.g., edit content node title).
2

Update IndexedDB

Resource API updates IndexedDB:
await ContentNode.update(nodeId, { title: 'New Title' });
3

Track change

Dexie Observable captures the change event automatically.
4

Debounced sync

A debounced sync function in frontend/shared/data/serverSync.js batches changes.
5

POST to sync endpoint

Changes are posted to /api/sync/:
{
  "changes": [
    {
      "table": "contentnode",
      "type": "UPDATED",
      "key": "abc123",
      "obj": { "title": "New Title" }
    }
  ]
}
6

Backend validates and applies

The sync endpoint validates changes and calls appropriate viewset methods (bulk_update, bulk_create, etc.).
7

Return updates to frontend

Server sends back any additional changes (e.g., updated timestamps) for IndexedDB.

Sync Mechanism

The sync system enables offline-first editing:
Four change types are tracked:
# From viewsets/sync/constants.py
CREATED = 1
UPDATED = 2
DELETED = 3
MOVED = 4

IndexedDB Resources

Frontend data is persisted in IndexedDB tables managed by Dexie.js.

Resource API

Resources are defined in frontend/shared/data/resources.js:
import { Resource } from 'shared/data/resources';

const Channel = new Resource({
  tableName: 'channel',
  urlName: 'channel',  // Matches DRF base_name
  idField: 'id',
  indexFields: ['public', 'edit'],  // IndexedDB indexes
  uuid: true,  // Generate UUIDs for new entries
  syncable: true,  // Sync changes to backend
});

Common Resources

ResourceTablePurpose
ChannelchannelChannel metadata
ContentNodecontentnodeContent items in tree
FilefileFile metadata
AssessmentItemassessmentitemExercise questions
UseruserUser information

Vuex Integration

Vuex stores listen to IndexedDB changes via Dexie Observable:
// frontend/channelEdit/vuex/contentNode/index.js
export default {
  namespaced: true,
  state: () => ({
    contentNodesMap: {},
  }),
  mutations: {
    ADD_CONTENTNODE(state, node) {
      state.contentNodesMap[node.id] = node;
    },
  },
  listeners: {
    [TABLE_NAMES.CONTENTNODE]: {
      [CHANGE_TYPES.CREATED]: 'ADD_CONTENTNODE',
      [CHANGE_TYPES.UPDATED]: 'ADD_CONTENTNODE',
    },
  },
};
When IndexedDB changes, registered mutations fire automatically to update UI.

Data Flow Diagram

Key Technologies

Dexie.js

IndexedDB wrapper for client-side storage

Dexie Observable

Track IndexedDB changes automatically

Django REST Framework

REST API framework for Django

Celery

Distributed task queue for async operations

Next Steps

Frontend Development

Build Vue.js components and Vuex modules

Backend Development

Create Django models and viewsets

Build docs developers (and LLMs) love