Learnova · Documentation · version 1.0.0

Set up your school, step by step.

Online courses platform: a Flutter app for iOS and Android, a Next.js website with an Instructor dashboard and an Admin panel, and a background worker. You add your own keys; everything else is ready.

Welcome

Thank you for buying Learnova. This guide is written for beginners: follow the chapters in order and you will have the server, the website and the app running with your own name and keys.

What is in the download

FolderWhat it is
learnova_mobile_flutter/The mobile app (Flutter, iOS and Android). Learners browse and buy courses, watch video lessons (also offline), take quizzes, hand in assignments, ask questions, join live classes, earn certificates and XP, and subscribe to Learnova Plus.
learnova_web_nextjs/One Next.js app that is the website (course store, checkout, the learning player), the Instructor dashboard (/instructor), the Admin panel (/admin) and the API the mobile app uses (/api/v1).
learnova_worker/Background jobs: turns uploaded videos into streaming (HLS) files, writes lesson transcripts, sends push notifications and live-class reminders, expires unpaid bank orders, shares Plus revenue with instructors, awards badges.
deploy/The Docker stack: website + worker + PostgreSQL database + MinIO file storage, started with one command.
tools/rename.mjsRebrands the whole kit (name, app id, colour, font, icon) in one command.
docs/This documentation (HTML and PDF).

What it looks like

The website home page

The Instructor dashboard

The Admin panel dashboard

The mobile app

How the parts fit

The mobile app and the website both talk to the same server (the Next.js app). All data lives in your PostgreSQL database. Lesson videos, thumbnails, files and certificates live in S3-compatible storage (MinIO in the Docker stack, or Amazon S3, Cloudflare R2, Backblaze B2…). Firebase is used only for sign-in, push notifications, Analytics and Crashlytics.

Nothing is blocked on a key. To try purchases before you add payment keys, set DEMO_PURCHASES="true": purchases then run in demo mode (courses and Plus are granted without payment and labelled DEMO). Keep it "false" on a live server. AI, email and push each show a clear "add your key" state until set.

Screenshots

The main screens, so you know what you are setting up. The names, photos and numbers are demo data; yours replace them.

Mobile app (iOS and Android)

Home
Home
Explore
Explore
Course
Course
Lesson player
Lesson player
Library
Library
Live class
Live class
Learnova Plus
Learnova Plus
Cart
Cart
Progress
Progress
Badges
Badges
Wallet
Wallet
Home (dark)
Home (dark)

Website

Home
Home
Catalog
Catalog
Course page
Course page
Checkout
Checkout

Instructor dashboard (/instructor)

Dashboard
Dashboard
Courses
Courses
Live classes
Live classes
Earnings
Earnings

Admin panel (/admin)

Dashboard
Dashboard
Courses
Courses
Orders
Orders
Payment gateways
Payment gateways
Staff and roles
Staff and roles
Reports
Reports

Requirements

To run the server

To build the mobile app

Third-party services (accounts you will create)

These services have their own pricing. Each one is run by another company, not by us, and has its own pricing tiers, usage limits and fees (for example per SMS sent, per payment taken, per map or AI request, or for storage and bandwidth). Most offer a free tier or a free test mode, but prices and limits can change at any time. You are responsible for any costs from these providers, as well as your server, domain, Apple Developer Program and Google Play Console accounts. Check each provider's pricing page before you go live.

What each service costs

Prices change, so check each provider's pricing page before you launch. This is how each one charges, so you know what grows with your school.

ServiceNeeded forHow it charges
Your server (VPS)Website, API, worker, database, file storageA fixed monthly price for the machine. A 2 vCPU / 4 GB server runs the whole Docker stack for a small school.
FirebaseSign-in, push, Analytics, CrashlyticsEmail, Google and Apple sign-in, push, Analytics and Crashlytics have no usage fee on the free plan. Phone (SMS) sign-in needs the pay-as-you-go plan and is charged per SMS.
File storage (MinIO, S3, R2, B2)Videos, thumbnails, files, certificatesMinIO in the Docker stack uses your server's disk (no extra fee). Cloud storage charges per GB stored per month, and most also per GB downloaded (Cloudflare R2 has no download fee).
Stripe, PayPal, Razorpay, Paystack, FlutterwaveWebsite paymentsA percentage plus a small fixed fee per successful payment; no monthly fee for the standard plans. Bank transfer and wallet cost nothing.
App Store and Google PlayPublishing the app, Plus in the appsApple Developer Program yearly fee; Google Play one-time registration fee. Both keep a commission on in-app purchases.
RevenueCatPlus subscriptions in the appsFree until your monthly tracked revenue passes their free limit, then a percentage of revenue.
OpenAI-compatible AI providerAI study buddy, AI answer drafts, transcriptsPer token (text) and per audio minute (transcripts). The daily free-question limit (Admin → Settings → Plus) caps what free learners can spend.
Email (any SMTP provider)Receipts, password links, staff invitesMost providers have a free monthly allowance, then a price per thousand emails.
Zoom, Google Meet, Microsoft TeamsLive classesLearnova only stores the meeting link; you use your own meeting account and its plan.

Quick start (Docker)

This gets the whole server running on your VPS in about 15 minutes. You need the Firebase keys from chapter 4 for sign-in; you can do this chapter first and add them after.

  1. Copy the kit to the server, for example with scp learnova-1.0.0.zip root@YOUR_SERVER_IP:, then on the server:
    apt install -y unzip
    unzip learnova-1.0.0.zip
    cd learnova/deploy
    cp .env.example .env
    nano .env
  2. In .env, fill at least these values:
    KeyWhat to put
    POSTGRES_PASSWORDA long random password (run openssl rand -hex 24 to make one).
    APP_URLYour website address, e.g. https://your-domain.com.
    MEDIA_SIGNING_SECRETAnother random value (openssl rand -hex 24).
    S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEYA user name and a long password you choose for the built-in MinIO storage.
    S3_PUBLIC_ENDPOINTThe address phones and browsers reach the storage on: https://storage.your-domain.com (see chapter 5), or http://YOUR_SERVER_IP:9000 for a first test.
    NEXT_PUBLIC_FIREBASE_*, FIREBASE_*From chapter 4.
  3. Start everything:
    docker compose up -d --build
    The first build takes 5–10 minutes. On the first start the database tables are created and sample data is loaded (courses, instructors, learning paths, Plus plans, pages, FAQ and a year of sample sales), so every screen has content.
  4. Check it: open http://YOUR_SERVER_IP:3000. You should see the home page with the sample courses. http://YOUR_SERVER_IP:3000/api/v1/health shows {"ok":true,"db":"ok",…}.
  5. Make yourself the admin (chapter 7), then put it on your domain with HTTPS (chapter 5).
Port 3000, 9000 or 9001 already used by something else on the server? Set WEB_PORT, MINIO_PORT and MINIO_CONSOLE_PORT in .env (and use the new storage port in S3_PUBLIC_ENDPOINT).
Changed .env? Run docker compose up -d again. Changed a NEXT_PUBLIC_… value? These are built into the website, so run docker compose up -d --build.

Firebase setup

Firebase handles sign-in (Google, Apple, email and guest) and push notifications. Your data stays in your own database.

Firebase is a third-party service with its own pricing. Firebase is run by Google, not by us. It has its own plans and pricing structure (the free Spark plan and the pay-as-you-go Blaze plan), and some features used here can cost money as you grow, for example Authentication beyond the free monthly active users, and Analytics or Cloud Messaging data exported to BigQuery. Prices and free limits can change at any time. You are responsible for any Firebase costs your project incurs. Check firebase.google.com/pricing before you go live, and set a budget alert in Google Cloud Billing.
  1. Go to the Firebase console → Add project. Give it your app name. Google Analytics: on.
  2. Build → Authentication → Get started → Sign-in method. Turn on: Email/Password (learners and staff), Google, Anonymous (guest browsing) and Apple (see chapter 18).
  3. Authentication → Settings → Authorized domains: add your-domain.com.
  4. Project settings (gear) → General → Your apps → Add app → Web (name it "Website"). Copy the values from the firebaseConfig shown into deploy/.env:
    NEXT_PUBLIC_FIREBASE_API_KEY="…apiKey…"
    NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your-project.firebaseapp.com"
    NEXT_PUBLIC_FIREBASE_PROJECT_ID="your-project"
    NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="your-project.firebasestorage.app"
    NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="…"
    NEXT_PUBLIC_FIREBASE_APP_ID="1:…:web:…"
  5. Project settings → Service accounts → Generate new private key. A JSON file downloads. Copy three values from it into deploy/.env:
    FIREBASE_PROJECT_ID="your-project"
    FIREBASE_CLIENT_EMAIL="firebase-adminsdk-xxxx@your-project.iam.gserviceaccount.com"
    FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n(your key)\n-----END PRIVATE KEY-----\n"
    Keep the \n as they are in the file. Keep this file secret.
  6. For the mobile app, install the FlutterFire CLI and connect the app (it registers the Android and iOS apps and writes their config files):
    dart pub global activate flutterfire_cli
    cd learnova_mobile_flutter
    flutterfire configure --project your-project --platforms android,ios \
      --android-package-name com.yourcompany.learnova --ios-bundle-id com.yourcompany.learnova
    Use your own app id here (see chapter 24 to change it first). Then copy the values into learnova_mobile_flutter/.env (FIREBASE_ANDROID_API_KEY, FIREBASE_ANDROID_APP_ID, FIREBASE_IOS_API_KEY, FIREBASE_IOS_APP_ID, FIREBASE_IOS_CLIENT_ID, FIREBASE_MESSAGING_SENDER_ID, FIREBASE_PROJECT_ID): you find them in android/app/google-services.json, ios/Runner/GoogleService-Info.plist, or in Project settings → Your apps.
  7. iOS Google sign-in: copy ios/Flutter/Firebase.xcconfig.example to ios/Flutter/Firebase.xcconfig and set GOOGLE_REVERSED_CLIENT_ID to the REVERSED_CLIENT_ID from GoogleService-Info.plist.
  8. Android Google sign-in needs your signing key fingerprints: run cd android && ./gradlew signingReport and add the SHA-1 and SHA-256 under Project settings → Your apps → Android → Add fingerprint. Add the fingerprints of your upload key and of Google Play's app signing key too when you publish.

Put it online (VPS, HTTPS)

Your server is a third-party service with its own pricing. The VPS, its bandwidth and your domain are billed by your hosting company and registrar, not by us. You are responsible for these costs. Video conversion needs CPU and streaming uses bandwidth, so pick a plan that grows with your learners.

The Docker stack listens on port 3000 (website) and 9000 (storage). Put a web server with free HTTPS in front of both. Caddy is the easiest:

apt install -y caddy
cat > /etc/caddy/Caddyfile <<'EOF'
your-domain.com {
  reverse_proxy localhost:3000
}
storage.your-domain.com {
  reverse_proxy localhost:9000
}
EOF
systemctl reload caddy

Then in deploy/.env set APP_URL="https://your-domain.com" and S3_PUBLIC_ENDPOINT="https://storage.your-domain.com", and run docker compose up -d. Caddy gets the certificates by itself.

Use HTTPS for both addresses in production. A website on https cannot play videos from an http storage address (browsers block it), and the iOS app only allows https.

Already use Traefik or Nginx? Point your-domain.com at port 3000 and storage.your-domain.com at port 9000 in the same way. Close ports 3000, 9000 and 9001 in your firewall once the proxy works (ufw allow 22,80,443/tcp && ufw enable). The MinIO console (port 9001) is for you only; reach it through an SSH tunnel: ssh -L 9001:localhost:9001 root@YOUR_SERVER_IP.

Backups

cd learnova/deploy
docker compose exec postgres pg_dump -U learnova learnova | gzip > backup-$(date +%F).sql.gz

Run it daily with cron and copy the files off the server. Videos and files are in the minio-data Docker volume; back it up too, or use a cloud bucket (next chapter).

Other hosting (Vercel, cloud storage)

Costs: Vercel and managed database providers are third-party services with their own pricing; you are responsible for any hosting costs.

Website on Vercel

Costs: Vercel and managed database providers are third-party services with their own pricing; you are responsible for any hosting costs.

  1. Create a PostgreSQL database (Neon, Supabase, Railway, or your own) and copy its connection string.
  2. Import learnova_web_nextjs into Vercel (Root directory: learnova_web_nextjs). Add every key of learnova_web_nextjs/.env.example under Settings → Environment Variables, with DATABASE_URL set to your database.
  3. Create the tables and the sample data once from your computer: in learnova_web_nextjs, put the same DATABASE_URL in .env, then pnpm install, pnpm prisma:migrate:deploy, pnpm first-run.
  4. The worker (video conversion, transcripts, push, reminders) needs ffmpeg and runs all the time, so it cannot run on Vercel. Run it on any small server with Docker: cd learnova_worker && docker build -t learnova-worker . && docker run -d --restart unless-stopped --env-file .env learnova-worker with the worker's .env filled from .env.example.

Cloud storage instead of MinIO

Any S3-compatible bucket works. Set S3_ENDPOINT (empty for Amazon S3; for Cloudflare R2 https://<account>.r2.cloudflarestorage.com), S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and leave S3_PUBLIC_ENDPOINT empty. Keep the bucket private, and add a CORS rule that allows GET and PUT from your domain (instructors upload straight to the bucket, and the website plays from it).

First admin and sample data

Create your own admin account (it makes the Firebase email account when it does not exist yet):

cd learnova/deploy
docker compose run --rm migrate pnpm admin:create you@example.com

It prints a link: open it to choose your password, then sign in at https://your-domain.com/admin/login. (You can also put the password after the email; it then shows on screen and in your shell history.) Without Docker: cd learnova_web_nextjs && pnpm admin:create you@example.com. Invite more staff from Admin → Roles & staff (chapter 19).

The sample data

The first start fills an empty database with sample categories, courses with lessons and quizzes, instructors, learners, learning paths, coupons, Plus plans, pages, FAQ and a year of sample sales, so you can see every screen working (dashboards, earnings, reports). Sample lessons play one short original clip until you upload real videos. When you are ready to go live:

Setup check

Open Admin → Setup: it lists every key, says which are missing, and tests each connection (database, storage, Firebase, push, every payment gateway, email, AI) with a clear message. The same check runs in a terminal:

docker compose run --rm migrate pnpm run doctor     # Docker
cd learnova_web_nextjs && pnpm run doctor          # without Docker

Use pnpm run doctor, not pnpm doctor (that one is pnpm's own check of your pnpm install).

Run the mobile app

  1. cd learnova_mobile_flutter, then copy .env.example to .env (the download already has one with empty values) and set:
    API_BASE_URL=https://your-domain.com
    APP_NAME=Learnova
    FIREBASE_…   (from chapter 4)
  2. Get the packages and run on a phone or emulator:
    flutter pub get
    flutter run
  3. Build for testing: flutter build apk --release (Android) or open ios/Runner.xcworkspace in Xcode (iOS).
  4. iOS signing: the Xcode project ships with Automatic signing and no team or provisioning profile set. Open ios/Runner.xcworkspace → select Runner → Signing & Capabilities → tick Automatically manage signing (already on) and choose your own Team. Xcode then creates the certificates and profiles for your bundle id. Do this for the Debug, Release and Profile configurations (the All tab).
Android build settings: android/app/build.gradle.kts sets compileSdk = 36, targetSdk = 36 and minSdk = 24 explicitly. Google Play requires a recent target SDK; when Google raises it, change both numbers there.
Running the server on your own computer? The Android emulator reaches it at http://10.0.2.2:7381 and the iOS simulator at http://localhost:7381 (pnpm dev uses port 7381; the Docker stack uses WEB_PORT, 3000 by default). Release builds need an https address.

The app reads every setting from .env: no keys live in Dart code. After changing .env, stop the app and run it again (hot reload does not reload .env).

Learnova Plus in the apps (RevenueCat)

Costs: RevenueCat is a third-party service with its own pricing plans; you are responsible for any RevenueCat costs.

Learners buy single courses, and can subscribe to Learnova Plus for every Plus course, unlimited AI questions, offline downloads and certificates. Inside the iOS and Android apps, Plus is sold through the App Store and Google Play with RevenueCat in between. The server is told about every purchase and gives access; the app never decides access itself.

  1. Create the subscriptions in App Store Connect and Google Play Console (one subscription group) with these ids, or your own (the ids are stored with each plan in Admin → Plans & Plus): learnova_plus_monthly, learnova_plus_yearly, learnova_plus_student.
  2. In RevenueCat: create a project, add your iOS and Android apps, import the products, create an entitlement plus with the Plus products, and an offering default (current) with the Plus packages.
  3. App keys: RevenueCat → Project → API keys → the public SDK keys. In learnova_mobile_flutter/.env: REVENUECAT_IOS_KEY, REVENUECAT_ANDROID_KEY, USE_REVENUE_CAT=true.
  4. Server keys in deploy/.env: REVENUECAT_API_KEY (a secret API key) and REVENUECAT_WEBHOOK_SECRET (any long random value).
  5. RevenueCat → Integrations → Webhooks → add https://your-domain.com/api/v1/webhooks/revenuecat with the Authorization header value equal to REVENUECAT_WEBHOOK_SECRET.
How money and credits are protected. The apps and the website never grant anything on their own. Course access, Plus and wallet top-ups come only from the store or payment gateway (webhook or a server-side check with the gateway); gift cards are codes stored on the server and redeemed once. Instructors' Plus pool share is paid by minutes watched, and the server checks every watch report: it never accepts more watched time than the clock allows since the learner's previous report (at the fastest playback speed), caps each lesson, and an instructor watching their own course earns no pool minutes. A guest merge needs the guest's own anonymous Firebase token, only a signed-in account can take in a guest, and each account takes in one guest at most.

Single courses and learning paths are paid in the app with the same checkout as the website: wallet, bank transfer, or the gateways from chapter 11, whose payment page opens in the browser. Courses bought on the website open in the app too. A share of each month's Plus revenue is paid to instructors by minutes watched; set the share in Admin → Settings → Learnova Plus.

Demo mode. With DEMO_PURCHASES="true" and no payment key set (RevenueCat, Stripe, PayPal, Razorpay, Paystack or Flutterwave), purchases are granted without payment and labelled DEMO, so you can try every flow. It is off by default, so a live server never gives courses or Plus away; adding any payment key also switches to live mode.

Website payments

Payment gateways are third-party services with their own pricing. Stripe, PayPal, Razorpay, Paystack and Flutterwave each take a fee per payment (and sometimes for refunds, currency conversion or payouts), need a verified business account in a supported country, and can change their prices at any time. You are responsible for these fees. Bank transfer costs nothing. Check each pricing page (see What each service costs) before you go live.

On the website, learners pay for courses, learning paths and Plus with any gateway you set up. Each gateway appears at checkout when its keys are set; switch them on or off in Admin → Settings → Payment gateways (the Configure button shows which keys are set and the webhook address to copy).

GatewayKeysWebhook URL and events
Stripe (cards, Apple Pay, Google Pay; Plus as a real subscription)STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECREThttps://your-domain.com/api/v1/webhooks/stripe with events checkout.session.completed, checkout.session.expired, invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted.
PayPalPAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_MODE=live (anything else = sandbox)None needed: the payment is confirmed when the buyer returns.
Razorpay (India)RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECREThttps://your-domain.com/api/v1/webhooks/razorpay, event payment_link.paid, with the same secret.
Paystack (Nigeria, Ghana)PAYSTACK_SECRET_KEYhttps://your-domain.com/api/v1/webhooks/paystack (event charge.success; signed with your secret key).
Flutterwave (Africa)FLUTTERWAVE_SECRET_KEY, FLUTTERWAVE_WEBHOOK_HASHhttps://your-domain.com/api/v1/webhooks/flutterwave; set the same Secret hash in Flutterwave → Settings → Webhooks.
Bank transferNone: set your bank details in Admin → Settings → Payment gateways → Bank transferThe buyer sees your details and a reference; you approve the order in Admin → Orders when the money arrives. Unpaid orders cancel after the hold time.

PayPal, Razorpay, Paystack and Flutterwave take one-time payments, so Plus bought with them is a pass for one period that the learner renews by buying again. Every payment is credited once, even when the return page and the webhook both arrive. Refunds from Admin → Orders go back through the same gateway; learners ask for refunds from their order page, within the window you set.

Currency and tax

Push notifications

Push uses Firebase Cloud Messaging, a third-party service with its own pricing. Sending push messages is free today, but Firebase is run by Google and its prices and limits can change; Apple push needs a paid Apple Developer Program membership. You are responsible for any costs.
  1. Android works once Firebase is set up (chapter 4).
  2. iOS: in the Apple Developer site create an APNs key (Keys → +, Apple Push Notifications service), then upload it in Firebase → Project settings → Cloud Messaging → Apple app configuration.
  3. The app registers each signed-in device and joins two topics: learnova_all and learnova_<language>. Send campaigns from Admin → Push notifications to everyone, Plus members, a course's learners, or one learner, now or scheduled. Learners also get pushes for replies, live-class reminders and announcements, and can turn each kind off in the app.
  4. The topic prefix is PUSH_TOPIC_PREFIX; it must be the same in deploy/.env and the app's .env (the rename tool keeps them equal).

Video lessons and storage

Storage is a third-party service with its own pricing (or your own disk). MinIO on your server uses your disk and bandwidth; cloud storage such as Amazon S3, Cloudflare R2 or Backblaze B2 charges for storage, requests and outgoing traffic. Each lesson is kept in up to four qualities plus a download copy, and every view streams from storage, so this is usually the largest cost as you grow. You are responsible for these costs.

Instructors upload lesson videos in Instructor → Courses → Curriculum. The file goes straight to your storage; the worker then makes streaming versions (1080p down to 360p, HLS) and a poster, and the lesson plays in the app and on the website with signed links that expire. Learners with Plus can download lessons for offline viewing in the app. Lesson files (PDFs, code) and assignment uploads are stored the same way.

Without storage keys, uploads are switched off and the sample lessons have no video. Admin → Setup tells you which key is missing.

AI study buddy and transcripts

The AI provider is a third-party service with its own pricing. OpenAI and other OpenAI-compatible providers charge per request (per token) and per minute of transcribed audio, and can change prices at any time. You are responsible for these costs. Keep the daily limit per learner in Admin → Settings → AI, and set a spending limit in your provider's dashboard.

Learners ask the AI study buddy about a lesson; instructors get draft answers in Q&A and quiz question drafts; the worker writes lesson transcripts. Set AI_API_KEY (and AI_BASE_URL for a provider other than OpenAI — Gemini, OpenRouter and Groq offer OpenAI-compatible URLs), and optionally AI_MODEL. For transcripts set SUBTITLE_PROVIDER=openai (any Whisper-compatible API). Free learners get a daily number of AI questions (Admin → Settings → Learnova Plus); Plus members have no limit. Without a key, the AI buttons say "add your key".

Live classes

Meeting services are third-party services with their own pricing. Learnova stores the meeting link; the meeting itself runs on Zoom, Google Meet or Microsoft Teams under that service's plan (free plans limit length and attendees). You are responsible for any costs.

Instructors schedule live sessions in Instructor → Live classes with a meeting link (Zoom, Google Meet, Teams…; a default link is set once in Instructor → Settings → Integrations). Booked learners see the session in the app and on the website, get a reminder before it starts (the time is set in Admin → Settings → Live classes), and join with one tap. Cohort courses sell seats for a scheduled run.

Email

Your email provider is a third-party service with its own pricing. Most SMTP providers have a free tier, then charge per email or per month. You are responsible for these costs.

Receipts, refund decisions, instructor application and course review results, staff invites, password resets and contact-form messages are sent by email. Set SMTP_HOST, SMTP_PORT (587, or 465 with SMTP_SECURE=true), SMTP_USER, SMTP_PASS from any provider (Amazon SES, Postmark, Mailgun, Brevo, your host). Set the sender name and address in Admin → Settings → Email (SMTP) and press Send test email. Without SMTP_HOST, emails are written to the server log instead (docker compose logs web), which is handy while testing.

Sign-in methods

Sign-in providers have their own pricing. Firebase Authentication is free up to a monthly user limit and needs the pay-as-you-go Blaze plan beyond it; phone sign-in SMS are charged per message; Sign in with Apple needs a paid Apple Developer Program membership. You are responsible for these costs.

Staff and roles

Admin → Roles & staff lists your team. Invite someone by email and pick a role; they get a link to choose a password (valid 72 hours, and you can resend it). Built-in roles: Super admin (everything, the only role that manages staff), Content reviewer, Support, Finance and Marketing. Open a role to tick, per area (courses, orders, payouts, settings…), what it may view, create, edit, delete and approve, or create your own roles. The admin menu only shows the areas a role can open. Every change made in the admin is recorded in Admin → Audit log.

Instructors apply from the website (/teach); you approve them in Admin → Instructors and set their revenue share. Their courses wait in Admin → Courses → In review until you approve them (or turn on "Publish new courses without review" in Settings → Learning & payouts).

Instructor studio: publish a course step by step

Every instructor works in the studio at /instructor on the website. This is the whole path from an empty course to one learners can buy.

The curriculum editor in the instructor studio

  1. New course. Studio → Courses → New course. Give it a title and a subject; it starts as a draft only you can see.
  2. Details. Subtitle, description, what learners will learn, requirements, level, language, captions. Upload a thumbnail (the cover photo, 1280×720 or larger) and, if you like, a short promo video.
  3. Curriculum. Add sections, then lessons in each: Video (upload the file; the worker turns it into streaming quality levels and writes the transcript when AI is set up), Article, Quiz (questions, the right answers, a pass mark), Assignment (a brief, a due date, points) or Live (a session in a cohort). Drag to reorder. Tick Free preview on a lesson anyone may watch before buying. Attach downloadable files to any lesson.
  4. Pricing. Set the price, an optional sale price with an end date, and whether the course is part of Learnova Plus. Instructors can also make coupons for their own courses.
  5. Submit for review. The checklist on the course page must be complete first: at least 5 lessons, every video processed, a thumbnail, a subtitle and a description. The course then waits in Admin → Courses → In review.
  6. Review. An admin approves it (it goes live in the catalogue) or requests changes with notes; the instructor fixes and submits again. To skip review on your own school, turn on "Publish new courses without review" in Admin → Settings → Learning & payouts.
A cohort is a course with a start date, a seat limit and live sessions. When it is full, new learners join a waitlist; if a seat opens, the first one in line gets it held for 24 hours.

Certificates

A learner gets a certificate automatically when they have completed every lesson of a course that offers one, and passed its final quiz if it has one. Finishing every course in a learning path adds a path certificate. Each certificate has an id like LNV-2026-04217 and a public page at /verify/<id> with a QR code, so an employer can check it.

Earnings, the Plus pool and payouts

Instructors earn in two ways. All amounts are kept in whole cents, so totals always add up.

SourceWhat the instructor getsSetting
Course salesTheir share of each sale after tax and discounts (70% by default; you can set a different share per instructor in Admin → Instructors).Admin → Settings → Learning & payouts → Instructor share
Learnova PlusEach month the worker puts a part of that month's Plus revenue in a pool (50% by default) and splits it by the minutes learners watched each instructor's Plus courses.Admin → Settings → Plus → Pool share
RefundsA refunded sale takes its share back.Refund window: 30 days, and only while less than 30% of the course was watched
  1. New earnings are on hold for the refund window (30 days by default), then become available.
  2. The instructor sets a payout method in Studio → Settings and requests a payout once the available balance reaches the minimum ($50 by default).
  3. An admin with the payouts permission marks it paid (after sending the money), puts it on hold, or rejects it in Admin → Payouts. Every step is in the audit log.

Admin panel tour

The admin panel is at /admin. Across the top are the modules; under them, the pages of the current module. Press ⌘K (Ctrl+K on Windows) anywhere to jump to a course, learner or order.

The admin daily brief

ModuleWhat you do there
OverviewThe daily brief: this month's sales, new learners and Plus members in one sentence, the month ledger, the revenue chart, everything waiting for review and the latest orders.
CatalogueReview and approve courses, manage subjects, approve instructor applications, moderate flagged reviews.
People & moneyLearners (suspend, enrol for free), orders and refunds, bank transfers to confirm, payouts, coupons, Plus plans, gift cards.
ContentCertificates and templates, blog, home banners, push campaigns, pages (terms, privacy, refunds, about) and help-centre FAQs.
InsightsReports: revenue, enrolments, top courses and instructors, with CSV export.
SystemSettings (general, learning and payouts, payment gateways, email, languages, currency and tax, Plus, live classes, AI, storage), roles and staff, the audit log and the setup check.

Rebrand: name, app id, colours

One command changes the app name everywhere (app, website, admin, emails, push topics, store product ids), the Android application id and iOS bundle id, the brand colour, the website font and the icons:

cd learnova
node tools/rename.mjs --name "MyAcademy" --id com.mycompany.myacademy --color "#0EA5E9" --font "DM Sans" --icon my-icon-1024.png

Every option is optional, so you can run it again later for one change. Add --dry to see what would change. Afterwards:

cd learnova_mobile_flutter
flutter pub get
dart run flutter_launcher_icons
dart run flutter_native_splash:create
flutterfire configure --project your-project   # Firebase files for the new app id

and rebuild the website (docker compose up -d --build). The folder names (learnova_mobile_flutter and so on) stay as they are; they are never shown to users.

By hand: the brand colours are in learnova_mobile_flutter/lib/core/theme/app_colors.dart (app) and learnova_web_nextjs/src/app/globals.css (website, --brand, --brand-soft, --brand-ink); the app name is APP_NAME in the app's .env and NEXT_PUBLIC_APP_NAME on the server; the name in emails, receipts and certificates is Admin → Settings → General → Platform name.

Basic edits

I want to change…Where
Categories, their icons, colours and topicsAdmin → Categories
Plus plans, prices, trial days, perksAdmin → Plans & Plus
Instructor share, refund window, payout hold and minimum, XP rewards, default quiz pass markAdmin → Settings → Learning & payouts
Coupons and gift cardsAdmin → Coupons, Admin → Gift cards (instructors make coupons for their own courses)
Home bannersAdmin → Banners
Terms, privacy, refund policy, account deletion, about, FAQAdmin → Pages & FAQ
Blog postsAdmin → Blog
Certificate designAdmin → Certificates
App versions (force update), maintenance mode, guest mode, store linksAdmin → Settings → General
Course languages instructors can chooseAdmin → Settings → Languages
Analytics or marketing scripts on the websiteAdd them in learnova_web_nextjs/src/app/(site)/layout.tsx and load them only when readCookieChoice() (from src/components/site/cookie-consent.tsx) says the visitor allowed analytics / marketing
App textsThe page files in learnova_mobile_flutter/lib/features/*/pages/
Website textsThe page files in learnova_web_nextjs/src/app/(site)/ and src/components/site/

Every key, explained

"Where" says which .env file it goes in: Docker = deploy/.env (shared by website and worker in the Docker stack), website = learnova_web_nextjs/.env (without Docker), worker = learnova_worker/.env (without Docker), app = learnova_mobile_flutter/.env. Admin → Setup shows the same list with the live status of each key.

KeyRequiredWhereWhat it does
Database
DATABASE_URLYeswebsite, workerPostgreSQL connection string
DATABASE_POOL_MAX—websiteConnections per server process (default 10)
POSTGRES_DBYesDockerDatabase the Docker stack creates
POSTGRES_USERYesDockerDatabase user for the Docker stack
POSTGRES_PASSWORDYesDockerDatabase password for the Docker stack
App
APP_URLYeswebsite, DockerPublic URL of the web app (links in emails, signed media)
NEXT_PUBLIC_APP_NAME—website, DockerName shown in the web app
NEXT_PUBLIC_IOS_APP_URL—website, DockerApp Store link for the website's Get the app buttons
NEXT_PUBLIC_ANDROID_APP_URL—website, DockerGoogle Play link for the website's Get the app buttons
WEB_PORT—DockerHost port the web container listens on
Storage
MINIO_PORT—DockerHost port of the built-in MinIO storage API (default 9000)
MINIO_CONSOLE_PORT—DockerHost port of the MinIO web console (default 9001)
App
CORS_ORIGINS—website, DockerBrowser apps allowed to call /api/v1
PUSH_TOPIC_PREFIX—website, Docker, appFCM topic prefix (<prefix>all, <prefix><language>); same as the server's
Firebase sign-in
NEXT_PUBLIC_FIREBASE_API_KEYYeswebsite, DockerFirebase web config
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYeswebsite, DockerFirebase web config
NEXT_PUBLIC_FIREBASE_PROJECT_IDYeswebsite, DockerFirebase web config
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET—website, DockerFirebase web config
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID—website, DockerFirebase web config
NEXT_PUBLIC_FIREBASE_APP_ID—website, DockerFirebase web config (sign-in works without it)
NEXT_PUBLIC_FIREBASE_ADMIN_APP_ID—website, DockerOptional second web app for the admin panel
NEXT_PUBLIC_AUTH_APPLE—website, Docker"true" shows Continue with Apple on the website (turn the Apple provider on in Firebase first)
ADMIN_REQUIRE_MFA—website, DockerRequire two-step sign-in for staff
Firebase Admin + push
FIREBASE_PROJECT_IDYeswebsite, worker, Docker, appFirebase project id
FIREBASE_CLIENT_EMAILYeswebsite, worker, DockerService account e-mail
FIREBASE_PRIVATE_KEYYeswebsite, worker, DockerService account private key
Secrets
MEDIA_SIGNING_SECRETYeswebsite, DockerSigns media URLs (openssl rand -hex 24)
REVENUECAT_WEBHOOK_SECRET—website, DockerAuthorization header RevenueCat sends to the webhook
Payments
REVENUECAT_API_KEY—website, DockerRevenueCat secret API key (in-app purchases)
STRIPE_SECRET_KEY—website, DockerStripe secret key (web checkout)
STRIPE_WEBHOOK_SECRET—website, DockerStripe webhook signing secret
PAYPAL_CLIENT_ID—website, DockerPayPal REST app client id (web checkout)
PAYPAL_CLIENT_SECRET—website, DockerPayPal REST app secret
PAYPAL_MODE—website, Docker"live" for real money, anything else = sandbox
RAZORPAY_KEY_ID—website, DockerRazorpay key id (web checkout, India)
RAZORPAY_KEY_SECRET—website, DockerRazorpay key secret
RAZORPAY_WEBHOOK_SECRET—website, DockerRazorpay webhook secret (payment_link.paid)
FLUTTERWAVE_SECRET_KEY—website, DockerFlutterwave secret key (web checkout, Africa)
FLUTTERWAVE_WEBHOOK_HASH—website, DockerFlutterwave webhook secret hash (verif-hash header)
PAYSTACK_SECRET_KEY—website, DockerPaystack secret key (web checkout, Nigeria / Ghana); also verifies its webhooks
DEMO_PURCHASES—website, Docker"true" grants purchases without payment while no payment key is set (testing only)
App
DEMO_MODE—website, worker, Docker"true" only on a public demo: one-click demo sign-in and read-only demo staff
SEED_SALES_HISTORY—website"false" skips the year of sample sales the seed adds for charts
Storage
S3_ENDPOINT—website, workerS3-compatible endpoint (empty for AWS S3)
S3_REGION—website, worker, DockerBucket region
S3_BUCKETYeswebsite, worker, DockerBucket for lesson videos, thumbnails, files and replays
S3_ACCESS_KEY_IDYeswebsite, worker, DockerStorage access key
S3_SECRET_ACCESS_KEYYeswebsite, worker, DockerStorage secret key
S3_PUBLIC_ENDPOINT—website, DockerStorage address phones and browsers use (required with the Docker MinIO)
S3_PUBLIC_URL—website, DockerPublic bucket / CDN base URL (leave empty for a private bucket)
Sign in with Apple
APPLE_TEAM_ID—website, DockerToken revocation at account deletion
APPLE_KEY_ID—website, DockerToken revocation at account deletion
APPLE_CLIENT_ID—website, DockeriOS bundle id
APPLE_PRIVATE_KEY_PATH—websitePath to the .p8 key (or use APPLE_PRIVATE_KEY)
APPLE_PRIVATE_KEY—website, DockerThe .p8 key contents
AI
AI_API_KEY—website, worker, DockerKey for any OpenAI-compatible API: study buddy, quiz drafts, Q&A drafts, transcripts
AI_BASE_URL—website, worker, DockerAPI base URL (empty = OpenAI; Gemini, OpenRouter and Groq have compatible URLs)
AI_MODEL—website, DockerChat model (default gpt-4o-mini)
AI_TRANSCRIBE_MODEL—worker, DockerTranscription model (default whisper-1)
SUBTITLE_PROVIDER—website, worker, Docker"openai" to generate lesson transcripts with Whisper
Worker
WORKER_CONCURRENCY—workerParallel transcodes
Email
SMTP_HOST—website, DockerSMTP server; empty = console mode
SMTP_PORT—website, Docker587 (STARTTLS) or 465 (TLS)
SMTP_USER—website, DockerSMTP user
SMTP_PASS—website, DockerSMTP password
SMTP_SECURE—website, Docker"true" for port 465
Mobile app
API_BASE_URLYesappYour server; the app calls <url>/api/v1
APP_NAME—appApp name in the UI
APP_STORE_ID—appRate-us and share links
REVENUECAT_IOS_KEY—appRevenueCat public iOS SDK key
REVENUECAT_ANDROID_KEY—appRevenueCat public Android SDK key
USE_REVENUE_CAT—appUse store purchases (false = demo)
LINK_PRIVACY—appPrivacy policy URL
LINK_TERMS—appTerms URL
LINK_SUPPORT—appSupport URL
LINK_DELETE_ACCOUNT—appAccount deletion URL
SUPPORT_EMAIL—appSupport e-mail
FIREBASE_MESSAGING_SENDER_IDYesappFirebase config
FIREBASE_STORAGE_BUCKET—appFirebase config
FIREBASE_WEB_API_KEY—appFirebase web config (Flutter web)
FIREBASE_WEB_APP_ID—appFirebase web config (Flutter web)
FIREBASE_WEB_AUTH_DOMAIN—appFirebase web config (Flutter web)
FIREBASE_ANDROID_API_KEYYesappFirebase Android config
FIREBASE_ANDROID_APP_IDYesappFirebase Android config
FIREBASE_IOS_API_KEYYesappFirebase iOS config
FIREBASE_IOS_APP_IDYesappFirebase iOS config
FIREBASE_IOS_CLIENT_ID—appGoogle sign-in on iOS
FIREBASE_IOS_BUNDLE_ID—appiOS bundle id
Store tooling
APPLE_APP_ID—appfastlane: App Store app id
ANDROID_APP_ID—appfastlane: Android package
APPLE_ID—appfastlane: Apple account
APPLE_ITC_TEAM_ID—appfastlane: App Store Connect team
APPLE_DEV_TEAM_ID—appfastlane: developer team

File structure

Mobile app (learnova_mobile_flutter/lib)

main.dart                 starts Firebase, loads .env, runs the app (short on purpose)
app.dart                  theme, router and app-wide listeners
components/               shared widgets (buttons, sheets, cards, tab bar) — names start with cc_
core/
  constants/              every .env value the app reads, app-wide constants
  router/                 all routes (go_router)
  theme/                  colours, text styles, icons, spacing
  providers/              app-wide state (Riverpod): session, config, subscription, sync…
  services/api/           the typed API client for your server (/api/v1) and its offline cache
  services/firebase/      sign-in, notifications, analytics
  services/general/       RevenueCat purchases (or demo mode), reviews, sharing
domain/models/            data models parsed from the API
features/                 one folder per area, each with pages/ widgets/ providers/
  launch  auth  home  explore  course  commerce  learning  player
  study  live  progress  downloads  profile

Lists and grids use the lazy .builder constructors, and state flows through Riverpod providers rather than long chains of widget parameters.

Server (learnova_web_nextjs/src)

app/(site)/               the public website (home, courses, paths, checkout, library, orders, Plus…)
app/learn/                the full-screen course player
app/instructor/           Instructor dashboard pages
app/admin/                Admin panel pages
app/api/v1/[...path]/     the single API entry point; routes live in lib/server/handlers/
components/site|instructor|admin|panel|ui   website, dashboard and admin components, base UI (shadcn)
lib/server/handlers/      every API route, by area: catalog, learning, quiz, commerce, plus, engagement, ai,
                          instructor, admin, system
lib/server/               auth, payments (store.ts, gateways.ts, commerce.ts), storage (media.ts), email, AI,
                          certificates (pdf.ts), settings, setup check
lib/api/schemas/          request validation (Zod)
database/prisma_client/   database schema (schema.prisma) and migrations
database/seed/            sample data and the sample clip
scripts/                  doctor, first-run, admin:create, seed:sales
tests/                    API tests (Vitest); e2e/ has the browser smoke tests (Playwright)

Worker (learnova_worker/src)

index.ts                  job queue (pg-boss on your PostgreSQL) and schedules
jobs/transcode.ts         uploaded video → HLS 1080p–360p + MP4 + poster (ffmpeg)
jobs/transcript.ts        lesson transcripts (Whisper-compatible API)
jobs/push.ts              push campaigns and personal notifications
jobs/live.ts              live-class reminders
jobs/orders.ts            expires unpaid bank-transfer orders
jobs/plus-pool.ts         monthly Plus revenue share for instructors
jobs/badges.ts, purge.ts  achievement badges, clean-up

Extend it: a new API route and a new worker job

A worked example: a "course of the day" that the worker picks every morning and the app can read.

1. The API route

Routes live in learnova_web_nextjs/src/lib/server/handlers/, one file per area, and every file is imported once in src/lib/server/api.ts. Add to handlers/catalog.ts:

/** The course of the day, picked by the worker every morning. */
route("GET", "/course-of-the-day", async () => {
  const row = await prisma.setting.findUnique({ where: { group_key: { group: "daily", key: "course" } } });
  if (!row) throw errors.notFound("Course of the day");
  const [card] = await courseCards({ id: row.value as string });
  return json({ course: card ?? null });
});

It answers at GET /api/v1/course-of-the-day. Need a signed-in user? Start the handler with const user = await auth.user();. Staff only? await auth.staff("courses", "view") — the area and action match the permission grid in Admin → Roles & staff.

2. The worker job

Jobs live in learnova_worker/src/jobs/. Add "course-of-the-day" to the QUEUES list in learnova_worker/src/index.ts, write the job, and schedule it next to the others:

// learnova_worker/src/jobs/daily.ts
import { query } from "../db";

export async function pickCourseOfTheDay(): Promise<number> {
  const [c] = await query<{ id: string }>("SELECT id FROM courses WHERE status = 'published' ORDER BY random() LIMIT 1");
  if (!c) return 0;
  await query(
    `INSERT INTO settings ("group", key, value, updated_at) VALUES ('daily', 'course', $1::jsonb, now())
     ON CONFLICT ("group", key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
    [JSON.stringify(c.id)]
  );
  return 1;
}

// learnova_worker/src/index.ts: import it at the top, then add after the other schedules
await every("course-of-the-day", "0 6 * * *", pickCourseOfTheDay, "course of the day picked");

Restart the worker (docker compose up -d --build worker). For a job the website starts instead of a schedule, call enqueue("your-queue", { … }) from src/lib/server/jobs.ts in the API and boss.work("your-queue", …) in the worker.

3. Show it in the app

Add a provider in lib/core/providers/data_providers.dart that reads ref.watch(apiClientProvider).get('/course-of-the-day'), and watch it in the Today page (lib/features/home/pages/home_page.dart) with ref.watch(...).

Run pnpm test in learnova_web_nextjs after a change: the tests start from a fresh database and cover checkout, coupons, payments, refunds, quizzes, certificates and permissions.

API reference

The website, the instructor studio, the admin panel and the mobile app all use one JSON API under /api/v1. The 293 routes, grouped by area. "Auth" says who may call it: Signed in needs a learner's Firebase ID token (Authorization: Bearer …, or the website session cookie); Instructor an approved instructor; Staff: area · action a staff role with that permission; Signed by the provider a payment provider's webhook signature.

MethodPathAuthWhat it does
System, sign-in and help
GET/api/v1/healthNoHealth check: the database answers and the app version
GET/api/v1/configNoRemote config every client reads on start (minimum versions, paywall, AI limits, goals)
POST/api/v1/auth/sessionNoExchange a Firebase ID token for the website's session cookie
DELETE/api/v1/auth/sessionNoSign out of the website (clears the session cookie)
POST/api/v1/auth/demoNoLive demo only (DEMO_MODE=true; 404 otherwise, so buyers never ship it on)
GET/api/v1/pages/:slugNoCMS pages (terms, privacy, refund policy, about) for the apps and the website
GET/api/v1/helpNoHelp centre articles and FAQs, with search and a category filter
GET/api/v1/media/:a/:bSigned link (expires)Signed media
GET/api/v1/media/:a/:b/:cSigned link (expires)Signed media
GET/api/v1/media/:a/:b/:c/:dSigned link (expires)Signed media
GET/api/v1/media/:a/:b/:c/:d/:eSigned link (expires)Signed media
Account
GET/api/v1/meSigned inSigning in again (every client calls GET /me after sign-in) cancels a scheduled deletion
POST/api/v1/me/avatarSigned in"Change photo": presigned PUT into storage; then PATCH /me { avatarUrl: fileUrl }
PATCH/api/v1/meSigned inUpdate your name, photo, bio or learning goal
GET/api/v1/me/preferencesSigned inYour notification and learning preferences
PATCH/api/v1/me/preferencesSigned inOnboarding (interests, goal), appearance, learning, notifications
POST/api/v1/me/devicesSigned inRegister this device for push notifications
DELETE/api/v1/me/devices/:tokenSigned inStop push notifications to a device
POST/api/v1/me/merge/previewSigned inCompares the guest's progress with the account's; the app then calls POST /me/merge
POST/api/v1/me/mergeSigned in"Merge both" keeps the higher streak and every course, note and XP; "Keep account" drops the guest's progress
POST/api/v1/me/exportSigned inA download link (one hour) for a copy of all your data as JSON
DELETE/api/v1/meSigned inReal accounts are scheduled for deletion in 30 days (signing in again cancels it); guests are deleted now
POST/api/v1/me/apple/revokeSigned inSign in with Apple revocation at account deletion (App Store rule)
Catalogue, paths, wishlist and blog
GET/api/v1/homeOptionalThe Today screen: continue card, goal, the last seven days, banners, live sessions and shelves
GET/api/v1/categoriesNoSubjects with their colours and course counts
GET/api/v1/coursesNoSearch and filter the catalogue (subject, level, price, length, rating, cohorts), paged
GET/api/v1/search/suggestNoPopular searches (Admin → Settings) and live suggestions for what is typed
GET/api/v1/instructorsNoInstructors, most followed first
GET/api/v1/instructors/:slugOptionalAn instructor's profile and courses
PUT/api/v1/instructors/:id/followSigned inFollow an instructor
DELETE/api/v1/instructors/:id/followSigned inUnfollow an instructor
GET/api/v1/site/statsNoCatalogue numbers for the website (courses, enrolments, rating, teachers)
GET/api/v1/site/testimonialsNoFive-star reviews picked for the website
GET/api/v1/courses/:idOptionalA course page: curriculum, instructor, reviews summary, cohort seats and what you own
GET/api/v1/courses/:id/reviewsNoA course's reviews, newest or most helpful first
PUT/api/v1/courses/:id/reviewSigned inWrite review: enrolled learners only, one review each (posting again edits it)
POST/api/v1/courses/:id/enrollSigned inFree courses and Plus courses: enroll without an order
PUT/api/v1/courses/:id/waitlistSigned inJoin the waitlist of a full cohort; position = order of joining
DELETE/api/v1/courses/:id/waitlistSigned inLeave a full cohort's waitlist
GET/api/v1/pathsNoLearning paths
GET/api/v1/paths/:slugOptionalA learning path with its courses in order and the bundle price
GET/api/v1/wishlistSigned inYour wishlist
PUT/api/v1/wishlist/:courseIdSigned inAdd a course to your wishlist
DELETE/api/v1/wishlist/:courseIdSigned inRemove a course from your wishlist
GET/api/v1/blogNoPublished blog posts
GET/api/v1/blog/:slugNoA blog post
Learning: player, notes and Q&A
GET/api/v1/my/coursesSigned inYour library: in progress, completed, cohorts or archived
PATCH/api/v1/my/courses/:courseIdSigned inArchive or restore a course in your library
GET/api/v1/learn/:courseIdOptionalPlayer bootstrap: the course, its curriculum with locks and ticks, and where to resume
GET/api/v1/lessons/:idOptionalA lesson to play: signed video links, files, your position and the next lesson
GET/api/v1/lessons/:id/transcriptOptionalA lesson's transcript with timestamps
GET/api/v1/lessons/:id/downloadSigned inOffline downloads: a signed MP4 of the chosen quality, for owned or Plus courses
POST/api/v1/lessons/:id/progressSigned inWatch reports every ~15 s: resume position, watched time checked against the clock (streaks, analytics, Plus pool), auto-complete at 90 %
POST/api/v1/lessons/:id/completeSigned in"Mark complete" and article lessons
GET/api/v1/lessons/:id/notesSigned inYour notes on a lesson
GET/api/v1/courses/:id/notesSigned inAll your notes in a course
POST/api/v1/lessons/:id/notesSigned inAdd a note at a moment in the video
PATCH/api/v1/notes/:idSigned inEdit one of your notes
DELETE/api/v1/notes/:idSigned inDelete one of your notes
GET/api/v1/lessons/:id/questionsNoQuestions asked on a lesson
POST/api/v1/lessons/:id/questionsSigned inAsk the instructor a question on a lesson
GET/api/v1/questions/:idNoA question with its answers
POST/api/v1/questions/:id/answersSigned inAnswer a question (learners in the course and its instructor)
Quizzes, assignments and uploads
GET/api/v1/quizzes/:idSigned inA quiz and your open attempt
POST/api/v1/quizzes/:id/attemptsSigned inStarts an attempt, or resumes the open one (answers save as you go)
PUT/api/v1/attempts/:id/answersSigned inSave answers on an open quiz attempt
POST/api/v1/attempts/:id/submitSigned inSubmit a quiz attempt; the server marks it
GET/api/v1/attempts/:idSigned inA marked attempt with the right answers and explanations
POST/api/v1/uploadsSigned inAn upload link for an assignment file or a profile photo (type and size checked first)
GET/api/v1/assignments/:idSigned inAn assignment brief and your submission
PUT/api/v1/assignments/:id/submissionSigned inHand in or update your assignment
AI study buddy
GET/api/v1/ai/usageSigned inQuestions left today on the AI study buddy
POST/api/v1/ai/askSigned inAsk the AI study buddy about a lesson (counts toward the daily limit)
POST/api/v1/ai/summarySigned inAI summary of a lesson
POST/api/v1/ai/quizSigned inAI practice questions for a lesson
Live classes, progress, certificates, notifications
GET/api/v1/liveOptionalUpcoming live sessions in your cohorts, and replays
GET/api/v1/live/:idOptionalA live session: time, join link when it opens, replay
PUT/api/v1/live/:id/reminderSigned inRemind me before a live session
DELETE/api/v1/live/:id/reminderSigned inCancel a live session reminder
POST/api/v1/live/:id/joinSigned inWaiting room → "Join": the meeting link opens from 10 minutes before the start until the end
GET/api/v1/live/:id/calendar.icsNoA live session as a calendar file
GET/api/v1/cohorts/:courseId/calendar.icsNo"Add all to calendar": every session of a cohort
GET/api/v1/cohorts/:courseId/hubSigned inCohort hub: week, next session, sessions done, this week's items, discussion, classmates
GET/api/v1/progressSigned inYour streak, XP, weekly minutes and goal history
POST/api/v1/progress/freezeSigned inUse a streak freeze for a missed day
GET/api/v1/badgesSigned inBadges you earned and the ones still locked
GET/api/v1/leaderboardSigned inWeekly / monthly XP from the XP ledger; "classmates" = learners who share a course with you
GET/api/v1/certificatesSigned inYour certificates
GET/api/v1/verify/:codeNoPublic: anyone can check a certificate id (QR on the PDF)
GET/api/v1/certificates/:code/pdfNoDownload a certificate as a PDF
GET/api/v1/notificationsSigned inYour notifications
POST/api/v1/notifications/readSigned inMark notifications as read
DELETE/api/v1/notifications/:idSigned inDelete a notification
POST/api/v1/supportOptionalSend a message to support
POST/api/v1/feedbackOptionalSend feedback about the app
POST/api/v1/newsletterNoNewsletter box: signing up twice is fine, and signing up again after unsubscribing turns it back on
POST/api/v1/teach/applySigned inApply to teach
GET/api/v1/teach/applicationSigned inThe status of your application to teach
Cart, checkout, orders and wallet
GET/api/v1/cartSigned inYour cart with prices, discounts and tax
POST/api/v1/cartSigned inAdd a course or path to your cart
DELETE/api/v1/cart/:itemIdSigned inRemove an item from your cart
POST/api/v1/cart/quoteSigned inCoupon, wallet and tax preview for the cart or a "Buy now" item
GET/api/v1/checkout/methodsOptionalThe payment methods this server can take right now
POST/api/v1/checkoutSigned inPays for the cart (or one course/path)
POST/api/v1/checkout/confirmSigned inPayPal, Razorpay, Paystack and Flutterwave hand the buyer back with a query; verify with the gateway and settle once
GET/api/v1/ordersSigned inYour orders
GET/api/v1/orders/:codeSigned inOne of your orders
GET/api/v1/orders/:code/invoiceSigned inDownload an order's invoice as a PDF
POST/api/v1/orders/:code/refundSigned inAsk for a refund within the refund window
POST/api/v1/orders/:code/proofSigned in"Upload proof of payment": attaches the receipt to a bank-transfer order for the admin
POST/api/v1/orders/:code/cancelSigned inCancel an unpaid order (gives a coupon use back)
GET/api/v1/walletSigned inYour wallet balance and history
POST/api/v1/wallet/topupSigned inStart a wallet top-up through a payment gateway
POST/api/v1/wallet/redeemSigned inRedeem a gift card into your wallet
Learnova Plus and payment webhooks
GET/api/v1/plansNoPaywall: plans with prices from the server; the app swaps in the store's localised price when it has one
GET/api/v1/plusSigned inManage subscription with its billing history
POST/api/v1/purchases/syncSigned inAsks RevenueCat for the subscriber and applies anything not credited yet (restore)
POST/api/v1/purchases/demoSigned inDEMO mode only: grants exactly what the store webhook would, without payment (store "demo")
POST/api/v1/plus/checkoutSigned inWebsite Plus: Stripe subscription, or a one-period pass through the other gateways
POST/api/v1/plus/portalSigned inOpen the Stripe billing portal for a website Plus subscription
POST/api/v1/webhooks/revenuecatSigned by the providerRevenueCat events for app subscriptions (shared secret)
POST/api/v1/webhooks/razorpaySigned by the providerRazorpay payment events (signature checked)
POST/api/v1/webhooks/paystackSigned by the providerPaystack payment events (signature checked)
POST/api/v1/webhooks/flutterwaveSigned by the providerFlutterwave payment events (verification hash checked)
POST/api/v1/webhooks/stripeSigned by the providerStripe events: checkout paid, subscription renewed or ended (signature checked)
Instructor: courses and curriculum
GET/api/v1/instructor/meInstructorYour instructor profile and counters for the studio
GET/api/v1/instructor/dashboardInstructorStudio dashboard: revenue, students, ratings and what needs you
GET/api/v1/instructor/coursesInstructorYour courses with status and sales
POST/api/v1/instructor/coursesInstructorStart a new course (draft)
GET/api/v1/instructor/courses/:idInstructorBuilder: details, curriculum with every lesson's state, pricing, checklist, review notes
PATCH/api/v1/instructor/courses/:idInstructorEdit a course's details, thumbnail and promo video
PUT/api/v1/instructor/courses/:id/pricingInstructorSet a course's price, sale and Plus inclusion
DELETE/api/v1/instructor/courses/:idInstructorDelete a draft course
POST/api/v1/instructor/courses/:id/unpublishInstructorUnpublish a course (owners keep access)
POST/api/v1/instructor/courses/:id/publishInstructorPublish an approved course that is waiting unlisted
POST/api/v1/instructor/courses/:id/submitInstructorSubmit for review (or publish straight away when Admin → Settings auto-approves courses)
POST/api/v1/instructor/courses/:id/sectionsInstructorAdd a section
PATCH/api/v1/instructor/sections/:idInstructorRename a section
DELETE/api/v1/instructor/sections/:idInstructorDelete a section and its lessons
PUT/api/v1/instructor/courses/:id/sections/orderInstructorSave the order of sections and lessons
POST/api/v1/instructor/courses/:id/lessonsInstructorAdd a lesson (video, article, quiz, assignment or live)
GET/api/v1/instructor/lessons/:idInstructorA lesson in the editor
PATCH/api/v1/instructor/lessons/:idInstructorEdit a lesson
DELETE/api/v1/instructor/lessons/:idInstructorDelete a lesson
PUT/api/v1/instructor/sections/:id/lessons/orderInstructorDrag and drop inside a section, or into another section: `ids` is the section's new order
POST/api/v1/instructor/lessons/:id/filesInstructorAttach a downloadable file to a lesson
DELETE/api/v1/instructor/files/:idInstructorRemove a lesson file
POST/api/v1/instructor/uploadsInstructorGet an upload link for a video, thumbnail or file
POST/api/v1/instructor/uploads/:id/completeInstructorConfirm an upload; videos go to the worker for transcoding
POST/api/v1/instructor/uploads/:id/failedInstructor"Upload failed": the client marks a dropped upload so the lesson shows the resume state
POST/api/v1/instructor/lessons/:id/transcriptInstructorInstructor captions: generate a transcript from the video (needs SUBTITLE_PROVIDER), "Auto captions"
GET/api/v1/instructor/lessons/:id/quizInstructorA quiz in the editor
PUT/api/v1/instructor/lessons/:id/quizInstructorEdit a quiz's settings (pass mark, attempts, time limit)
POST/api/v1/instructor/lessons/:id/quiz/questionsInstructorAdd a quiz question
PUT/api/v1/instructor/questions/:idInstructorEdit a quiz question
DELETE/api/v1/instructor/questions/:idInstructorDelete a quiz question
PUT/api/v1/instructor/lessons/:id/quiz/orderInstructorSave the order of quiz questions
POST/api/v1/instructor/ai/quiz-draftInstructor"Generate with AI": drafts questions from a lesson's transcript; the instructor reviews before saving
GET/api/v1/instructor/lessons/:id/assignmentInstructorAn assignment in the editor
PUT/api/v1/instructor/lessons/:id/assignmentInstructorEdit an assignment's brief, due date and marking
Instructor: students, Q&A, live, coupons, earnings
GET/api/v1/instructor/couponsInstructorYour coupons
POST/api/v1/instructor/couponsInstructorCreate a coupon for your courses
PUT/api/v1/instructor/coupons/:idInstructorEdit one of your coupons
DELETE/api/v1/instructor/coupons/:idInstructorDelete one of your coupons
GET/api/v1/instructor/studentsInstructorLearners in your courses with progress
GET/api/v1/instructor/students/export.csvInstructorYour learners as a CSV file
GET/api/v1/instructor/students/:enrollmentIdInstructorOne learner's progress, quiz scores and submissions
GET/api/v1/instructor/questionsInstructorQuestions waiting for you
GET/api/v1/instructor/questions/:idInstructorA question thread
POST/api/v1/instructor/questions/:id/draftInstructor"AI draft from your lesson transcript": a suggestion only; the instructor edits and posts it
POST/api/v1/instructor/questions/:id/answersInstructorAnswer a question
POST/api/v1/instructor/questions/:id/hideInstructorHide a question
GET/api/v1/instructor/submissionsInstructorAssignments waiting to be marked
GET/api/v1/instructor/submissions/:idInstructorA submission with its files
POST/api/v1/instructor/submissions/:id/gradeInstructorMark a submission and leave feedback
GET/api/v1/instructor/announcementsInstructorYour announcements
POST/api/v1/instructor/announcementsInstructorSend an announcement to a course's learners
GET/api/v1/instructor/liveInstructorYour live sessions
POST/api/v1/instructor/liveInstructorSchedule a live session (Zoom, Meet or Teams link)
PATCH/api/v1/instructor/live/:idInstructorEdit a live session or add its replay
DELETE/api/v1/instructor/live/:idInstructorCancel a live session
GET/api/v1/instructor/earningsInstructorYour earnings: course sales, Plus minutes, refunds
GET/api/v1/instructor/earnings/export.csvInstructorYour earnings as a CSV file
GET/api/v1/instructor/payoutsInstructorYour payouts and available balance
POST/api/v1/instructor/payoutsInstructorRequest a payout of your available balance
GET/api/v1/instructor/settingsInstructorYour payout method and studio settings
PATCH/api/v1/instructor/settingsInstructorUpdate your payout method and studio settings
GET/api/v1/instructor/reviewsInstructorReviews on your courses
PUT/api/v1/instructor/reviews/:id/replyInstructorReply to a review
GET/api/v1/instructor/analyticsInstructorViews, enrolments, completion and watch time per course
Admin: catalogue, people and money
GET/api/v1/admin/meStaff: courses · viewThe signed-in staff member and what their role may do
GET/api/v1/admin/dashboardStaff: reports · viewAdmin dashboard: month figures, revenue chart, review queue, latest orders
GET/api/v1/admin/coursesStaff: courses · viewAll courses, by status (in review first)
GET/api/v1/admin/courses/:idStaff: courses · viewA course with its review checklist
POST/api/v1/admin/courses/:id/scanStaff: courses · view"AI moderation scan": the course's text checked by the configured model
POST/api/v1/admin/courses/:id/approveStaff: courses · approveApprove a course in review (publishes it)
POST/api/v1/admin/courses/:id/request-changesStaff: courses · approveSend a course back to its instructor with notes
PATCH/api/v1/admin/courses/:idStaff: courses · editChange a course's status (published, unlisted, archived) or badges
GET/api/v1/admin/courses-exportStaff: courses · viewAll courses as a CSV file
GET/api/v1/admin/categoriesStaff: categories · viewSubjects
POST/api/v1/admin/categoriesStaff: categories · createCreate a subject
PATCH/api/v1/admin/categories/:idStaff: categories · editEdit a subject
PUT/api/v1/admin/categories/orderStaff: categories · editSave the order of subjects
POST/api/v1/admin/categories/:id/deleteStaff: categories · deleteA category with courses can only go once its courses move to another one
GET/api/v1/admin/instructorsStaff: instructors · viewInstructors and applications
GET/api/v1/admin/instructors/:idStaff: instructors · viewAn instructor or application
POST/api/v1/admin/instructors/:id/approveStaff: instructors · approveApprove an application to teach
POST/api/v1/admin/instructors/:id/rejectStaff: instructors · approveReject an application to teach
PATCH/api/v1/admin/instructors/:idStaff: instructors · editEdit an instructor's profile or revenue share
GET/api/v1/admin/studentsStaff: students · viewLearners
GET/api/v1/admin/students-exportStaff: students · viewLearners as a CSV file
GET/api/v1/admin/students/:idStaff: students · viewA learner's courses, orders and activity
POST/api/v1/admin/students/:id/suspendStaff: students · editSuspend a learner
POST/api/v1/admin/students/:id/unsuspendStaff: students · editLift a learner's suspension
POST/api/v1/admin/students/:id/enrollStaff: students · editEnrol a learner in a course for free
GET/api/v1/admin/ordersStaff: orders · viewOrders, filtered by status, gateway and date
GET/api/v1/admin/orders-exportStaff: orders · viewOrders as a CSV file
GET/api/v1/admin/orders/:codeStaff: orders · viewAn order with its payments and refunds
POST/api/v1/admin/orders/:code/refundStaff: orders · approveRefund an order (all or part)
GET/api/v1/admin/refundsStaff: orders · viewRefund requests
POST/api/v1/admin/refunds/:id/decideStaff: orders · approveApprove or decline a refund request
POST/api/v1/admin/orders/:code/bankStaff: orders · approveBank transfer (orders "Pending"): approve once the money arrived, or decline (wallet money comes back)
POST/api/v1/admin/orders/:code/receiptStaff: orders · viewConfirm a bank transfer and fulfil the order
GET/api/v1/admin/payoutsStaff: payouts · viewInstructor payout requests
POST/api/v1/admin/payouts/approveStaff: payouts · approveApproves the selected requests; payouts on hold stay on hold
POST/api/v1/admin/payouts/:id/actionStaff: payouts · approveMark a payout paid, hold, release or reject it
GET/api/v1/admin/couponsStaff: coupons · viewCoupons
POST/api/v1/admin/couponsStaff: coupons · createPlatform coupons: the platform pays the discount, so instructors keep their share of the full price
PUT/api/v1/admin/coupons/:idStaff: coupons · editEdit a coupon
DELETE/api/v1/admin/coupons/:idStaff: coupons · deleteDelete a coupon
GET/api/v1/admin/plansStaff: settings · viewPlus plans
POST/api/v1/admin/plansStaff: settings · createCreate a Plus plan
PATCH/api/v1/admin/plans/:idStaff: settings · editEdit a Plus plan
GET/api/v1/admin/reviewsStaff: reviews · viewFlagged reviews
POST/api/v1/admin/reviews/:id/keepStaff: reviews · editKeep a flagged review
POST/api/v1/admin/reviews/:id/removeStaff: reviews · deleteRemove a review
Admin: content, settings and staff
GET/api/v1/admin/certificate-templatesStaff: certificates · viewCertificate templates
POST/api/v1/admin/certificate-templatesStaff: certificates · createCreate a certificate template
PUT/api/v1/admin/certificate-templates/:idStaff: certificates · editEdit a certificate template
DELETE/api/v1/admin/certificate-templates/:idStaff: certificates · deleteDelete a certificate template
POST/api/v1/admin/certificate-templates/previewStaff: certificates · view"Preview PDF": the template with sample data, before or after saving
POST/api/v1/admin/certificates/:code/revokeStaff: certificates · editRevoke a certificate (its public link says so)
GET/api/v1/admin/blogStaff: blog · viewBlog posts
GET/api/v1/admin/blog/:idStaff: blog · viewA blog post in the editor
POST/api/v1/admin/blogStaff: blog · createCreate a blog post
PUT/api/v1/admin/blog/:idStaff: blog · editEdit a blog post
DELETE/api/v1/admin/blog/:idStaff: blog · deleteDelete a blog post
GET/api/v1/admin/bannersStaff: banners · viewHome banners
POST/api/v1/admin/bannersStaff: banners · createCreate a home banner
PUT/api/v1/admin/banners/:idStaff: banners · editEdit a home banner
PUT/api/v1/admin/banners/orderStaff: banners · editSave the order of home banners
DELETE/api/v1/admin/banners/:idStaff: banners · deleteDelete a home banner
GET/api/v1/admin/pushStaff: banners · viewPush campaigns
POST/api/v1/admin/push/estimateStaff: banners · viewCount the devices a push campaign would reach
POST/api/v1/admin/pushStaff: banners · createSend or schedule a push campaign
POST/api/v1/admin/push/testStaff: banners · view"Send test to me": an inbox entry and a push to the signed-in staff member's own devices
DELETE/api/v1/admin/push/:idStaff: banners · deleteCancel a scheduled push campaign
GET/api/v1/admin/reportsStaff: reports · viewReports: revenue, enrolments, top courses and instructors
GET/api/v1/admin/reports/export.csvStaff: reports · viewReports as a CSV file
GET/api/v1/admin/settings/:groupStaff: settings · viewOne settings group (app, payments, email, AI, live, …)
PUT/api/v1/admin/settings/:groupStaff: settings · editSave one settings group
GET/api/v1/admin/settings-statusStaff: settings · viewWhich keys the server has (never their values) and each gateway's on/off switch
POST/api/v1/admin/settings/test-emailStaff: settings · editSend a test email with the saved mail settings
GET/api/v1/admin/tax-rulesStaff: settings · viewTax rules by country
POST/api/v1/admin/tax-rulesStaff: settings · createCreate a tax rule
PUT/api/v1/admin/tax-rules/:idStaff: settings · editEdit a tax rule
DELETE/api/v1/admin/tax-rules/:idStaff: settings · deleteDelete a tax rule
GET/api/v1/admin/languagesStaff: settings · viewCourse languages
POST/api/v1/admin/languagesStaff: settings · createAdd a course language
PATCH/api/v1/admin/languages/:codeStaff: settings · editTurn a course language on or off
GET/api/v1/admin/pagesStaff: blog · viewLegal and info pages
GET/api/v1/admin/pages/:slugStaff: blog · viewA page in the editor
PUT/api/v1/admin/pages/:slugStaff: blog · editSave a page
GET/api/v1/admin/faqsStaff: blog · viewHelp centre FAQs
POST/api/v1/admin/faqsStaff: blog · createCreate a FAQ
PUT/api/v1/admin/faqs/:idStaff: blog · editEdit a FAQ
DELETE/api/v1/admin/faqs/:idStaff: blog · deleteDelete a FAQ
GET/api/v1/admin/gift-cardsStaff: orders · viewGift cards
POST/api/v1/admin/gift-cardsStaff: orders · createCreate gift cards
GET/api/v1/admin/staffStaff: staff · viewStaff members and roles
POST/api/v1/admin/staff/inviteStaff: staff · createInvite a staff member by email
POST/api/v1/admin/staff/:id/resendStaff: staff · createResend a staff invitation
PATCH/api/v1/admin/staff/:idStaff: staff · editChange a staff member's role or turn their access off (super admin)
POST/api/v1/admin/staff/:id/reset-two-stepStaff: staff · edit"Can't use your authenticator?": a super admin clears the member's second factor; they enrol again after signing in
DELETE/api/v1/admin/staff/:idStaff: staff · deleteRemove a staff member
POST/api/v1/admin/rolesStaff: staff · createCreate a role with its permission grid
PUT/api/v1/admin/roles/:codeStaff: staff · editEdit a role's permissions
DELETE/api/v1/admin/roles/:codeStaff: staff · deleteDelete a role
GET/api/v1/admin/auditStaff: staff · viewThe audit log of staff actions
Admin: setup check
GET/api/v1/admin/setupStaff: settings · viewSetup check: which keys are set and what each one turns on
POST/api/v1/admin/setup/test-emailStaff: settings · editSends a test email to the signed-in staff member (super admin; the demo admin is read-only)
POST/api/v1/admin/auth/password-resetNoAlways 202 so the form never reveals who has an account
GET/api/v1/admin/auth/invite/:tokenNoTurns a 72-hour staff invite token into a fresh Firebase reset code; 410 when it expired or was replaced

Database (PostgreSQL)

All data lives in PostgreSQL, described in learnova_web_nextjs/src/database/prisma_client/schema.prisma and changed only through migrations (pnpm prisma:migrate:dev to make one, pnpm prisma:migrate:deploy to apply them; the Docker migrate service runs the second for you). Money is stored as whole cents with a currency. The 62 tables:

TableWhat it holdsKeysPoints to
People
usersEveryone who signs in: learners, instructors and staff (the Firebase uid links the account; staffRole makes staff).PK id
unique public_id
unique firebase_uid
staff_role → staff_roles
staff_rolesAdmin roles. permissions = { <area>: ["view","create","edit","delete","approve"] }.PK code—
devicesPush tokens for a user's phones and browsers.PK id
unique token
user_id → users
instructorsAn instructor profile; created by the Teach application, approved by admin.PK id
unique owner_id
unique slug
owner_id → users
Catalogue
categoriesSubjects in the catalogue, with their colour and icon.PK id
unique slug
—
coursesA course or live cohort: details, price, status, cohort dates and seat limit.PK id
unique slug
category_id → categories
instructor_id → instructors
sectionsA course's sections, in order.PK idcourse_id → courses
lessonsA lesson in a section: video, article, quiz, assignment or live, with its video keys and preview flag.PK idcourse_id → courses
section_id → sections
lesson_filesLesson resources: an uploaded file (key) or a link (url).PK idlesson_id → lessons
quizzesA quiz lesson's settings: pass mark, attempts and time limit.PK id
unique lesson_id
lesson_id → lessons
quiz_questionsA quiz question with its choices and the right answer (never sent to the player before marking).PK idquiz_id → quizzes
quiz_attemptsA learner's attempt at a quiz, with answers and the server's mark.PK idquiz_id → quizzes
user_id → users
assignmentsAn assignment lesson's brief, due date and points.PK id
unique lesson_id
lesson_id → lessons
submissionsA learner's hand-in for an assignment, with files, grade and feedback.PK id
unique (assignment_id, user_id)
assignment_id → assignments
user_id → users
learning_pathsA learning path: several courses in order, sold as a bundle.PK id
unique slug
—
learning_path_coursesThe courses in a learning path, in order.PK (path_id, course_id)path_id → learning_paths
course_id → courses
Learning
enrollmentsA learner's access to a course (bought, free, Plus, gift or admin), with progress and archive state.PK id
unique (user_id, course_id)
user_id → users
course_id → courses
lesson_progressWhere a learner is in a lesson and whether it is complete.PK (user_id, lesson_id)user_id → users
lesson_id → lessons
watch_daysSeconds watched per learner per course per day; feeds streaks, the weekly chart, analytics and the Plus pool.PK (user_id, course_id, day)—
notesA learner's note at a moment in a lesson.PK iduser_id → users
course_id → courses
lesson_id → lessons
questionsA question asked on a lesson (Q&A).PK iduser_id → users
course_id → courses
lesson_id → lessons
answersAn answer to a question, by a learner or the instructor.PK idquestion_id → questions
user_id → users
reviewsA star rating and review of a course, with the instructor's reply and flags.PK id
unique (course_id, user_id)
course_id → courses
user_id → users
follows"Follow": learners following an instructor (push audience "Followers of …", new course notices).PK (user_id, instructor_id)user_id → users
instructor_id → instructors
wishlist_itemsCourses a learner saved for later.PK (user_id, course_id)user_id → users
course_id → courses
cart_itemsCourses and paths in a learner's cart.PK id
unique (user_id, course_id)
unique (user_id, path_id)
user_id → users
course_id → courses
path_id → learning_paths
certificatesA certificate of completion with its public code for verification.PK id
unique code
unique (user_id, course_id)
user_id → users
course_id → courses
path_id → learning_paths
template_id → certificate_templates
certificate_templatesCertificate designs the admin can choose from.PK id—
Gamification
xp_eventsEvery XP grant, idempotent per (user, reason, ref). Weekly / monthly leaderboards sum these.PK id
unique (user_id, reason, ref_id)
user_id → users
daily_activityMinutes learned per learner per day, the goal and streak freezes.PK (user_id, day)user_id → users
badgesBadges learners can earn and their rules.PK code—
user_badgesBadges a learner has earned.PK (user_id, badge_code)user_id → users
badge_code → badges
Live & cohorts
live_sessionsA scheduled live class for a cohort: time, meeting link, replay.PK id
unique lesson_id
course_id → courses
instructor_id → instructors
lesson_id → lessons
live_remindersA learner's reminder for a live session.PK (user_id, session_id)user_id → users
session_id → live_sessions
waitlist_entriesA learner waiting for a seat in a full cohort, and any seat held for them.PK (user_id, course_id)user_id → users
course_id → courses
Money
couponsDiscount codes: percent or fixed, limits and dates (uses are counted atomically).PK id
unique code
instructor_id → instructors
ordersA purchase: lines, totals, tax, coupon, gateway and status.PK id
unique code
unique external_id
unique idempotency_key
user_id → users
order_itemsThe courses, paths, plans or top-ups in an order.PK idorder_id → orders
course_id → courses
path_id → learning_paths
instructor_id → instructors
refundsRefund requests and refunds on orders.PK idorder_id → orders
order_item_id → order_items
user_id → users
walletsA learner's wallet balance (cents, one currency).PK user_iduser_id → users
wallet_transactionsEvery wallet credit and debit, with the order or gift card behind it.PK id
unique idempotency_key
user_id → users
gift_cardsGift cards and their redemption.PK id
unique code
—
plansLearnova Plus plans. Teams is a display-only plan ("contact sales") in release 1.PK id
unique store_product_id
—
subscriptionsLearnova Plus subscriptions from the stores (RevenueCat) and the website (Stripe).PK id
unique external_id
user_id → users
plan_id → plans
tax_rulesTax rates by country (and region).PK id
unique (country, region)
—
earning_entriesInstructor earnings ledger: sales (70 %), Plus minutes, cohort seats, refunds. Available after the hold.PK id
unique ref
instructor_id → instructors
payoutsInstructor payout requests and their status.PK id
unique code
instructor_id → instructors
Engagement, content, system
notificationsIn-app notifications for a user.PK iduser_id → users
announcementsInstructor announcement to a course's learners: inbox + push, 2 per course per month.PK idinstructor_id → instructors
course_id → courses
push_campaignsPush campaigns sent or scheduled from the admin.PK id—
bannersHome banners in the app and on the website.PK id—
blog_postsBlog posts.PK id
unique slug
—
cms_pagesLegal and info pages (terms, privacy, refund policy, about).PK slug—
faqsHelp centre questions and answers.PK id—
languagesCourse languages that can be picked in the studio.PK code—
settingsKey/value remote config, grouped (app, learning, payments, plus, ai, live, email, storage).PK (group, key)—
support_messagesMessages sent to support and feedback from the apps and website.PK id
unique ticket
user_id → users
ai_messagesQuestions to the AI study buddy and its answers (counts the daily limit).PK iduser_id → users
uploadsFiles on their way to storage: who uploads what, type, size and status.PK id
unique key
—
audit_logsWhat each staff member changed, and when.PK idactor_id → users
course_daily_statsPer course per day: page views, enrollments, revenue, watch seconds (instructor analytics, admin reports).PK (course_id, day)course_id → courses
newsletter_subscribersBlog "Get one great lesson a week" sign-ups; exported from Admin → Reports.PK id
unique email
—

App permissions and store ids

The app asks for a permission only when the learner uses the feature. These are the texts the system shows; change them in the files named here before you publish (the stores reject vague texts).

PermissionWhereText in the kit
Photo library (iOS)ios/Runner/Info.plist → NSPhotoLibraryUsageDescriptionChoose a photo for your profile or attach an image to an assignment.
NotificationsAsked in the app after onboardingLive-class reminders, replies to your questions, streak reminders.
Internet, notifications (Android)android/app/src/main/AndroidManifest.xmlNo text needed.

Placeholder ids to replace

These ids are placeholders in the download. Replace each with your own before you publish.

WhatWherePlaceholder
Android application id / iOS bundle idnode tools/rename.mjs --id com.yourcompany.yourappcom.yourcompany.learnova
Firebase appsflutterfire configure (chapter Firebase setup)your-firebase-project in google-services.json and GoogleService-Info.plist
Plus subscription productsApp Store Connect, Google Play Console, then Admin → Plans & Pluslearnova_plus_monthly, learnova_plus_yearly, learnova_plus_student
RevenueCat keyslearnova_mobile_flutter/.envempty (REVENUECAT_IOS_KEY, REVENUECAT_ANDROID_KEY)

The app has no ads, so there are no ad unit ids to set.

Publish to the stores

You publish the app under your own developer accounts. Before you start: set your app id (chapter 24), your Firebase files (chapter 4), an https API_BASE_URL, and your privacy, terms and account deletion pages (Admin → Pages & FAQ; the stores ask for these links).

Google Play

  1. Create an upload key inside android/app: cd learnova_mobile_flutter/android/app && keytool -genkey -v -keystore upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload. Back this file up and never share it.
  2. Create android/key.properties:
    storePassword=…
    keyPassword=…
    keyAlias=upload
    storeFile=upload-keystore.jks
    Without this file, release builds are signed with the debug key (fine for testing, refused by Play).
  3. Set the version in pubspec.yaml (version: 1.0.0+1; raise the number after + for every upload), then flutter build appbundle.
  4. In Play Console: create the app, fill the store listing, content rating, data safety (the app collects account info, purchase history, learning progress, device ids for push, crash logs; the "Delete account URL" is https://your-domain.com/delete-account, its text is in Admin → Pages), and upload build/app/outputs/bundle/release/app-release.aab to a testing track first.
  5. Add the SHA-1/SHA-256 of Play's app signing key (Play Console → Test and release → App integrity) to Firebase.

App Store

  1. In the Apple Developer site, register your bundle id with Push Notifications, Sign in with Apple and In-App Purchase.
  2. Open ios/Runner.xcworkspace in Xcode → Runner → Signing & Capabilities → choose your team (Automatic signing is already on; chapter 9).
  3. In App Store Connect create the app, fill the listing and App Privacy, create the Plus subscriptions (chapter 10).
  4. flutter build ipa, then upload build/ios/ipa/*.ipa with the Transporter app, and send it to TestFlight first.
Store rules to keep in mind: subscriptions sold inside the app must use in-app purchases, which the app does for Plus. Paying for single courses through a web gateway from inside the app is allowed by Apple and Google only in some countries and programs (for example Apple's US storefront rules for external purchase links); read the current App Store Review Guidelines (3.1) and Google Play payments policy for your countries before you publish. Apps with sign-in must offer account deletion (Profile → Account → Delete account, included) and, on iOS, Sign in with Apple when Google sign-in is offered.

Updating

When a new version comes out, read the changelog, back up your database (chapter 5), then copy the new files over your copy, keeping your .env files, Firebase files and any changes you made. Run docker compose up -d --build: database changes are applied automatically on start. Using git for your copy makes this much easier: commit before you copy the update in, and review the differences.

FAQ and troubleshooting

Videos do not play / uploads fail

Check S3_PUBLIC_ENDPOINT: open https://storage.your-domain.com/minio/health/live in a browser; it must answer. With an https website the storage address must be https too. With a cloud bucket, check its CORS rule allows your domain.

An uploaded lesson stays "processing"

The worker converts videos. Check it runs: docker compose logs worker. The curriculum editor shows the reason when a conversion fails.

Sign-in fails on the website

Add your domain under Firebase → Authentication → Settings → Authorized domains, and check all NEXT_PUBLIC_FIREBASE_* values, then rebuild (docker compose up -d --build).

Google sign-in fails on Android

Add the SHA-1 and SHA-256 of the key that signed the build to Firebase, then download the config again (flutterfire configure).

Checkout says DEMO

No payment key is set yet. That is on purpose: add your gateway or RevenueCat keys (chapters 10–11).

A gateway does not show at checkout

Its keys are missing, or it is switched off in Admin → Settings → Payment gateways. Admin → Setup tests each gateway's keys.

Push notifications do not arrive

iOS needs the APNs key in Firebase. Check PUSH_TOPIC_PREFIX is the same on the server and in the app, and that the phone allowed notifications. Admin → Setup tests the Firebase push connection.

The app shows "Can't reach the server"

API_BASE_URL in the app's .env must be your site's address (https, no /api/v1 at the end). Open https://your-domain.com/api/v1/health on the phone's browser.

I changed .env and nothing happened

Server: docker compose up -d (add --build for NEXT_PUBLIC_*). App: stop and run again.

Where are the logs?

docker compose logs -f web and docker compose logs -f worker. App crashes go to Firebase Crashlytics.

Credits and licences

The services in Third-party services are separate companies, not part of this item. Each has its own pricing tiers and terms, and you are responsible for any costs from them.

Learnova is built on these open-source projects. Each keeps its own licence (mostly MIT, BSD-3 or Apache 2.0).

Mobile app

Flutter, flutter_riverpod, go_router, FlutterFire (firebase_core, firebase_auth, firebase_messaging, firebase_analytics, firebase_crashlytics, firebase_app_check), google_sign_in, sign_in_with_apple, purchases_flutter (RevenueCat), video_player, video_player_web_hls, hive_ce_flutter, http, flutter_dotenv, lucide_icons_flutter, shimmer, flutter_styled_toast, url_launcher, share_plus, in_app_review, package_info_plus, connectivity_plus, path_provider, wakelock_plus, image_picker, file_picker, flutter_markdown_plus, shared_preferences, intl, flutter_launcher_icons, flutter_native_splash.

Website, dashboards and worker

Next.js, React, Prisma, PostgreSQL, pg, pg-boss, Zod, Tailwind CSS, shadcn/ui, Base UI, Lucide icons, Sonner, next-themes, hls.js, react-markdown, pdf-lib, qrcode, Nodemailer, Firebase Admin SDK, FFmpeg, MinIO, Docker.

Fonts

Fraunces, Instrument Sans, IBM Plex Mono and Caveat (website notes), SIL Open Font License 1.1. The app bundles the first three with their licence files in learnova_mobile_flutter/assets/fonts/; the website loads them with next/font at build time.

Content

The sample course texts, quizzes, instructor and learner names, and the sample video clip are original and part of your licence. Pictures and videos on our live demo are for preview only and are not included in the download.

Changelog

1.0.0 — 2026-10-22

First release. See CHANGELOG.md in the download for the full list.

Support

Questions or a problem? Use the Support tab on the item page on CodeCanyon, with your purchase code, what you did, and what you saw (a screenshot and the output of pnpm run doctor help a lot). Support covers questions about the item, help with bugs, and help with the third-party services it uses as far as this documentation goes. It does not cover changes or new features you want to build; for that, or to have it installed for you, ask about our installation service on the item page.