How the backend is built and how to run it: the data model, every collection, all activity types, the publish pipeline, roles, the API, and how to operate it day to day.
Sign in at the studio URL with the email and password the team gives you. Then: read your dashboard, find content (now by name, not code), build an activity in a friendly form, preview it live, send it through review, and publish.
As you edit an activity, Live Preview plays it in the real app engine — same audio, same buttons. What you build is exactly what the teacher sees.
Content is organised into collections, grouped in the left nav. Records now show a readable name, not a slug. “Versioned” means the collection keeps drafts separate from what's published.
| Collection | What it is | Key fields | Versioned |
|---|---|---|---|
| Languages | Each target language (Halbi, Bhatri, Ho…) plus Hindi. | code, name, native_name | — |
| Learning Paths (Courses) | An ordered set of modules in one language, e.g. Language Fundamentals. | label, slug_id, title.hi, order, language | Drafts |
| Modules | A lesson within a course — the sequence of activities a learner completes together. | label, slug_id, title.hi, order, xp_display, learning_path | Drafts |
| Activities | A single learning interaction inside a module. | title_hi, module, type, order, payload (validated JSON) | Drafts |
| Words | The dictionary — words & phrases with audio used across activities. | text, transliteration, translation, audio, language | Drafts |
| Media | Uploaded audio & images. Pick from here inside any activity. | upload, alt, sha256/bytes/mime (auto) | — |
| Collection | What it is | Key fields | Notes |
|---|---|---|---|
| Learners | Teachers who registered in the app. | learner_ref, name, phone, language, role, subjects, state, profile_complete | Created by the app on sign-up |
| Learner Stats | A live projection of each learner's XP, level and streak. | total_xp, level, level_name, streak_days, last_event_at | Read-only (computed) |
| Xp Events | The progress ledger — one row per completed activity. | event_key, learner_ref, activity_ref, score, xp_earned, attempted_at | Immutable |
| Collection | What it is | Key fields | Notes |
|---|---|---|---|
| Packs | The published output the app downloads — one versioned bundle per module. | pack_id, pack_version, language, pack_doc, status | Immutable; current / superseded |
| Audit Log | Who published what, and when. | action, collection_slug, doc_id, actor_email, actor_role | Immutable |
| Otp Challenges | Short-lived phone-OTP records for app login. | phone, code_hash, expires_at, attempts | Hashed + peppered |
| Users | Studio author accounts. | email, password, role | admin / reviewer / editor |
Global · Gamification Config — one settings record: XP defaults (e.g. listening_quiz_per_question = 10, flash_card_per_card = 5) and the level-up thresholds (level · name · min_xp).
Each activity type is defined once as a JSON Schema (schemas/activity.<type>.schema.json). That single definition drives the authoring form, validates content on publish, and tells the app which mechanic (interaction engine) to render. Adding a type = add a schema.
| Mechanic | Activity type | What the learner does |
|---|---|---|
tap_select | Listening Quiz | Hears audio, taps the option that matches. |
tap_select | Fill in the Blank | Picks the word that completes a sentence (marked ____). |
tap_select | Word Cloud | Selects the right words from a scattered set. |
flip_swipe | Flash Cards | Taps to flip; swipes right = Known, left = Still Learning. |
match_pairs | Matching Game | Matches pairs (word ↔ meaning, word ↔ picture). |
place_label | Picture Labelling | Drags labels onto the right parts of a picture. |
assemble_order | Dialogue Builder | Drags chips into order to build a phrase that matches a meaning. |
passive_play | Instructions / Reading | Reads a passage — non-interactive. |
passive_play | Audio Narration | Listens to phrases (each with audio) and repeats aloud. |
passive_play | Video Snippet | Watches a short clip. |
record_compare | Pronunciation Imitation | Records their voice and compares to the model. |
You don't write JSON. Pick the Type and the form shows the right fields — text boxes, an options list with a “correct” toggle, and a media picker for audio/images. The studio validates it against the schema when you save.
Every content record carries a review status alongside its draft/published state. Only approved content can be published (admins may bypass). Moves between statuses are role-gated.
| Stage | Status | Who moves it forward |
|---|---|---|
| You write it | Draft | Editor |
| Send for review | In review | Editor → submits |
| Reviewer checks it | Approved or Changes requested | Reviewer decides |
| Goes live | Published | Reviewer or Admin (must be Approved) |
If a reviewer requests changes, it returns to the editor, who fixes it and re-submits. Publishing is always a deliberate, separate action from saving a draft.
An operator can collapse the workflow to admin-only publish by setting REVIEW_WORKFLOW_ENABLED=false — useful for a small launch team.
The app never talks to the database directly. Publishing a module builds a versioned pack — a self-contained bundle the app downloads and runs offline.
rebuildPack job fires automatically.current; the old one becomes superseded./api/pack-index, downloads the new pack, renders offline.Because the rebuild is asynchronous, a freshly published change appears in Packs as a new current version within moments, and reaches a device on its next sync.
| Capability | Editor | Reviewer | Admin |
|---|---|---|---|
| Create & edit content | ✓ | ✓ | ✓ |
| Send for review | ✓ | ✓ | ✓ |
| Approve / request changes | — | ✓ | ✓ |
| Publish (push to app) | — | ✓ | ✓ |
| Delete content | — | — | ✓ |
| Manage Users, Packs, System & Audit Log | — | — | ✓ |
Authors and edits content; submits it for review.
Works the review queue; approves or sends back; can publish.
Everything, plus users, packs, system collections and the audit trail.
The Flutter app talks to a small, stable API. Learner endpoints require a phone-OTP JWT; studio endpoints use the CMS session.
| Endpoint | Method | Purpose | Auth |
|---|---|---|---|
/api/auth/request-otp | POST | Send a login code to a phone. | Public |
/api/auth/verify-otp | POST | Verify the code, return a learner token. | Public |
/api/me | GET | Current learner's profile. | Learner |
/api/me/profile | POST | Update profile (name, language, role…). | Learner |
/api/me/stats | GET | XP, level and streak. | Learner |
/api/pack-index | GET | List the current packs to download. | Learner |
/api/pack/[packId] | GET | Download one versioned pack. | Learner |
/api/ingest-events | POST | Submit completed-activity events (progress). | Learner |
/api/activity-schema | GET | Schema for a type — powers the authoring form. | Studio |
/api/preview-activity | GET/POST | Build the preview shown in Live Preview. | Studio |
The studio is configured entirely through environment variables — see cms/.env.example. Secrets are generated per environment; the server refuses to start in production without the required ones.
| Variable | What it does | Required |
|---|---|---|
DATABASE_URI | Postgres connection string. | Yes |
PAYLOAD_SECRET | Encrypts studio sessions. | Yes |
AUTH_JWT_SECRET | Signs learner phone-OTP tokens (HS256). | Prod |
OTP_PEPPER | Extra secret mixed into the OTP hash before storage. | Prod |
NEXT_PUBLIC_SERVER_URL | The public URL of the studio. | Yes |
ASSET_BASE_URL | CDN base for media URLs in packs. | Yes |
REVIEW_WORKFLOW_ENABLED | true = full review gate; false = admin-only publish. | Optional |
current version; the device picks it up on its next sync.Spot a bug? Note the screen, what you did, and what you expected — and send it over. The Audit Log records every publish, which helps trace what changed.
tap_select, flip_swipe, match_pairs.learner_refcurrent; older ones are superseded but kept.