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
| Folder | What 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.mjs | Rebrands the whole kit (name, app id, colour, font, icon) in one command. |
docs/ | This documentation (HTML and PDF). |
What it looks like
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.
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)
Website
Instructor dashboard (/instructor)
Admin panel (/admin)
Requirements
To run the server
- A Linux server (VPS) with at least 2 CPU cores, 4 GB RAM and 40 GB disk (more disk for many videos). Ubuntu 24.04 is used in this guide.
- Docker with the Compose plugin (install guide).
- A domain name, for example
your-domain.com, with two DNS records pointing at the server:your-domain.comandstorage.your-domain.com.
To build the mobile app
- Flutter 3.44 or newer (install), Android Studio for Android, a Mac with Xcode 26 for iOS.
- Node.js 22 or newer and pnpm 11 (
npm i -g pnpm) if you run the website without Docker or use the rename tool.
Third-party services (accounts you will create)
- Firebase (required: sign-in and push).
- Optional, when you want them: RevenueCat (Plus subscriptions in the apps), Stripe / PayPal / Razorpay / Paystack / Flutterwave (website payments), any OpenAI-compatible AI provider (study buddy and transcripts), any SMTP email provider.
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.
| Service | Needed for | How it charges |
|---|---|---|
| Your server (VPS) | Website, API, worker, database, file storage | A fixed monthly price for the machine. A 2 vCPU / 4 GB server runs the whole Docker stack for a small school. |
| Firebase | Sign-in, push, Analytics, Crashlytics | Email, 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, certificates | MinIO 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, Flutterwave | Website payments | A 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 Play | Publishing the app, Plus in the apps | Apple Developer Program yearly fee; Google Play one-time registration fee. Both keep a commission on in-app purchases. |
| RevenueCat | Plus subscriptions in the apps | Free until your monthly tracked revenue passes their free limit, then a percentage of revenue. |
| OpenAI-compatible AI provider | AI study buddy, AI answer drafts, transcripts | Per 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 invites | Most providers have a free monthly allowance, then a price per thousand emails. |
| Zoom, Google Meet, Microsoft Teams | Live classes | Learnova 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.
- 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 - In
.env, fill at least these values:Key What to put POSTGRES_PASSWORDA long random password (run openssl rand -hex 24to 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), orhttp://YOUR_SERVER_IP:9000for a first test.NEXT_PUBLIC_FIREBASE_*,FIREBASE_*From chapter 4. - Start everything:
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.docker compose up -d --build - 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/healthshows{"ok":true,"db":"ok",…}. - Make yourself the admin (chapter 7), then put it on your domain with HTTPS (chapter 5).
WEB_PORT, MINIO_PORT and MINIO_CONSOLE_PORT in .env (and use the new storage port in S3_PUBLIC_ENDPOINT)..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.
- Go to the Firebase console → Add project. Give it your app name. Google Analytics: on.
- Build → Authentication → Get started → Sign-in method. Turn on: Email/Password (learners and staff), Google, Anonymous (guest browsing) and Apple (see chapter 18).
- Authentication → Settings → Authorized domains: add
your-domain.com. - Project settings (gear) → General → Your apps → Add app → Web (name it "Website"). Copy the values from the
firebaseConfigshown intodeploy/.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:…" - Project settings → Service accounts → Generate new private key. A JSON file downloads. Copy three values from it into
deploy/.env:
Keep theFIREBASE_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"\nas they are in the file. Keep this file secret. - For the mobile app, install the FlutterFire CLI and connect the app (it registers the Android and iOS apps and writes their config files):
Use your own app id here (see chapter 24 to change it first). Then copy the values intodart 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.learnovalearnova_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 inandroid/app/google-services.json,ios/Runner/GoogleService-Info.plist, or in Project settings → Your apps. - iOS Google sign-in: copy
ios/Flutter/Firebase.xcconfig.exampletoios/Flutter/Firebase.xcconfigand setGOOGLE_REVERSED_CLIENT_IDto theREVERSED_CLIENT_IDfromGoogleService-Info.plist. - Android Google sign-in needs your signing key fingerprints: run
cd android && ./gradlew signingReportand 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)
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.
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.
- Create a PostgreSQL database (Neon, Supabase, Railway, or your own) and copy its connection string.
- Import
learnova_web_nextjsinto Vercel (Root directory:learnova_web_nextjs). Add every key oflearnova_web_nextjs/.env.exampleunder Settings → Environment Variables, withDATABASE_URLset to your database. - Create the tables and the sample data once from your computer: in
learnova_web_nextjs, put the sameDATABASE_URLin.env, thenpnpm install,pnpm prisma:migrate:deploy,pnpm first-run. - 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-workerwith the worker's.envfilled 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:
- Unlist or archive the sample courses in Admin → Courses, and remove the sample instructors in Admin → Instructors, or keep them as placeholders while your own instructors upload.
- Check Plans & Plus, Coupons, Pages & FAQ (terms, privacy, refund policy) and Settings and edit them for your business.
- To start from a completely empty database instead, run
docker compose down -v(this deletes all data and videos), setSEED_SALES_HISTORY=falsein.envto skip the sample sales, thendocker compose up -dagain.
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
cd learnova_mobile_flutter, then copy.env.exampleto.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)- Get the packages and run on a phone or emulator:
flutter pub get flutter run - Build for testing:
flutter build apk --release(Android) or openios/Runner.xcworkspacein Xcode (iOS). - 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/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.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.
- 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. - In RevenueCat: create a project, add your iOS and Android apps, import the products, create an entitlement
pluswith the Plus products, and an offeringdefault(current) with the Plus packages. - 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. - Server keys in
deploy/.env:REVENUECAT_API_KEY(a secret API key) andREVENUECAT_WEBHOOK_SECRET(any long random value). - RevenueCat → Integrations → Webhooks → add
https://your-domain.com/api/v1/webhooks/revenuecatwith the Authorization header value equal toREVENUECAT_WEBHOOK_SECRET.
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_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
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).
| Gateway | Keys | Webhook URL and events |
|---|---|---|
| Stripe (cards, Apple Pay, Google Pay; Plus as a real subscription) | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | https://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. |
| PayPal | PAYPAL_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_SECRET | https://your-domain.com/api/v1/webhooks/razorpay, event payment_link.paid, with the same secret. |
| Paystack (Nigeria, Ghana) | PAYSTACK_SECRET_KEY | https://your-domain.com/api/v1/webhooks/paystack (event charge.success; signed with your secret key). |
| Flutterwave (Africa) | FLUTTERWAVE_SECRET_KEY, FLUTTERWAVE_WEBHOOK_HASH | https://your-domain.com/api/v1/webhooks/flutterwave; set the same Secret hash in Flutterwave → Settings → Webhooks. |
| Bank transfer | None: set your bank details in Admin → Settings → Payment gateways → Bank transfer | The 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
- Admin → Settings → Currency & tax: the checkout currency and whether prices include tax (EU, UK, AU style) or tax is added at checkout (US style).
- Add a tax rule per country, US state or Canadian province (and one "European Union" rule as a fallback for EU countries). Checkout uses the most specific active rule for the billing address, shows it on the receipt, and the Reports → Tax / VAT tab totals it.
Push notifications
- Android works once Firebase is set up (chapter 4).
- 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.
- The app registers each signed-in device and joins two topics:
learnova_allandlearnova_<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. - The topic prefix is
PUSH_TOPIC_PREFIX; it must be the same indeploy/.envand the app's.env(the rename tool keeps them equal).
Video lessons and storage
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.
AI study buddy and transcripts
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
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.
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
- Email and password: on in Firebase. Learners sign up, verify and reset their password from the app or website.
- Google: on in Firebase; Android needs the SHA fingerprints, iOS needs
GOOGLE_REVERSED_CLIENT_ID(chapter 4). - Guest: Firebase Anonymous. Learners browse and preview without an account; turn it on or off in Admin → Settings → General. A guest who signs up keeps their progress.
- Apple (required by Apple when you offer Google sign-in on iOS): Apple Developer → your app id → enable Sign in with Apple. In Firebase → Apple provider, add your Services ID, Team ID, Key ID and the .p8 key. On the server set
APPLE_TEAM_ID,APPLE_KEY_ID,APPLE_CLIENT_ID(your bundle id) andAPPLE_PRIVATE_KEYso account deletion also revokes the Apple sign-in, as Apple requires. SetNEXT_PUBLIC_AUTH_APPLE=trueto show it on the website too. - Staff sign in with email and password at
/admin/login. Two-step verification uses an authenticator app (Google Authenticator, 1Password, Authy…). To offer it: in the Google Cloud console upgrade Firebase Authentication to Identity Platform, then turn on TOTP in Firebase → Authentication → Sign-in method → Multi-factor authentication. Each staff member then opens the account menu (top right) in the admin → Two-step verification, scans the QR code and enters a code; from then on sign-in asks for a 6-digit code after the password. To make it compulsory setADMIN_REQUIRE_MFA=trueonce everyone is enrolled. Someone who loses their phone: a super admin opens them in Admin → Roles & staff and presses Reset under Two-step verification.
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.
- New course. Studio → Courses → New course. Give it a title and a subject; it starts as a draft only you can see.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
- Designs: Admin → Certificates → Templates. Pick the default template, its colours, signature name and title.
- Revoke: Admin → Certificates, open one and revoke it. The verify page then says it was revoked.
- Pass mark: Admin → Settings → Learning & payouts → "Certificate pass mark" (70% by default) is the starting pass mark for new quizzes.
Earnings, the Plus pool and payouts
Instructors earn in two ways. All amounts are kept in whole cents, so totals always add up.
| Source | What the instructor gets | Setting |
|---|---|---|
| Course sales | Their 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 Plus | Each 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 |
| Refunds | A refunded sale takes its share back. | Refund window: 30 days, and only while less than 30% of the course was watched |
- New earnings are on hold for the refund window (30 days by default), then become available.
- The instructor sets a payout method in Studio → Settings and requests a payout once the available balance reaches the minimum ($50 by default).
- 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.
| Module | What you do there |
|---|---|
| Overview | The 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. |
| Catalogue | Review and approve courses, manage subjects, approve instructor applications, moderate flagged reviews. |
| People & money | Learners (suspend, enrol for free), orders and refunds, bank transfers to confirm, payouts, coupons, Plus plans, gift cards. |
| Content | Certificates and templates, blog, home banners, push campaigns, pages (terms, privacy, refunds, about) and help-centre FAQs. |
| Insights | Reports: revenue, enrolments, top courses and instructors, with CSV export. |
| System | Settings (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.
Logo, icon and fonts
- App icon and splash: replace the PNGs in
learnova_mobile_flutter/assets/icon/(1024×1024:icon_ios.pngwithout transparency,icon_android.png,icon_foreground.pngfor Android adaptive icons,icon_splash.png), then rundart run flutter_launcher_iconsanddart run flutter_native_splash:create. The splash and adaptive-icon background colours are influtter_native_splash.yamlandpubspec.yaml. - Website icons: replace
learnova_web_nextjs/src/app/icon.png(512×512),apple-icon.png(180×180) andfavicon.ico. The logo mark next to the name issrc/components/shared/logo.tsx. - App fonts: the app uses three families: Fraunces for headings and big numbers (
display), Instrument Sans for everything else (family) and IBM Plex Mono for small labels and counters (mono). To change one, put your.ttffiles inassets/fonts/, list them underfonts:inpubspec.yaml, and change the matching name at the top oflib/core/theme/app_text.dart. Check the font's licence allows apps. - Website fonts:
--fontin the rename tool changes the interface font (Instrument Sans); the heading serif, the mono labels and the handwriting used for learner notes are the othernext/font/googleimports inlearnova_web_nextjs/src/app/layout.tsx.
Basic edits
| I want to change… | Where |
|---|---|
| Categories, their icons, colours and topics | Admin → Categories |
| Plus plans, prices, trial days, perks | Admin → Plans & Plus |
| Instructor share, refund window, payout hold and minimum, XP rewards, default quiz pass mark | Admin → Settings → Learning & payouts |
| Coupons and gift cards | Admin → Coupons, Admin → Gift cards (instructors make coupons for their own courses) |
| Home banners | Admin → Banners |
| Terms, privacy, refund policy, account deletion, about, FAQ | Admin → Pages & FAQ |
| Blog posts | Admin → Blog |
| Certificate design | Admin → Certificates |
| App versions (force update), maintenance mode, guest mode, store links | Admin → Settings → General |
| Course languages instructors can choose | Admin → Settings → Languages |
| Analytics or marketing scripts on the website | Add 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 texts | The page files in learnova_mobile_flutter/lib/features/*/pages/ |
| Website texts | The 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.
| Key | Required | Where | What it does |
|---|---|---|---|
| Database | |||
DATABASE_URL | Yes | website, worker | PostgreSQL connection string |
DATABASE_POOL_MAX | — | website | Connections per server process (default 10) |
POSTGRES_DB | Yes | Docker | Database the Docker stack creates |
POSTGRES_USER | Yes | Docker | Database user for the Docker stack |
POSTGRES_PASSWORD | Yes | Docker | Database password for the Docker stack |
| App | |||
APP_URL | Yes | website, Docker | Public URL of the web app (links in emails, signed media) |
NEXT_PUBLIC_APP_NAME | — | website, Docker | Name shown in the web app |
NEXT_PUBLIC_IOS_APP_URL | — | website, Docker | App Store link for the website's Get the app buttons |
NEXT_PUBLIC_ANDROID_APP_URL | — | website, Docker | Google Play link for the website's Get the app buttons |
WEB_PORT | — | Docker | Host port the web container listens on |
| Storage | |||
MINIO_PORT | — | Docker | Host port of the built-in MinIO storage API (default 9000) |
MINIO_CONSOLE_PORT | — | Docker | Host port of the MinIO web console (default 9001) |
| App | |||
CORS_ORIGINS | — | website, Docker | Browser apps allowed to call /api/v1 |
PUSH_TOPIC_PREFIX | — | website, Docker, app | FCM topic prefix (<prefix>all, <prefix><language>); same as the server's |
| Firebase sign-in | |||
NEXT_PUBLIC_FIREBASE_API_KEY | Yes | website, Docker | Firebase web config |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | website, Docker | Firebase web config |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | website, Docker | Firebase web config |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | — | website, Docker | Firebase web config |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | — | website, Docker | Firebase web config |
NEXT_PUBLIC_FIREBASE_APP_ID | — | website, Docker | Firebase web config (sign-in works without it) |
NEXT_PUBLIC_FIREBASE_ADMIN_APP_ID | — | website, Docker | Optional 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, Docker | Require two-step sign-in for staff |
| Firebase Admin + push | |||
FIREBASE_PROJECT_ID | Yes | website, worker, Docker, app | Firebase project id |
FIREBASE_CLIENT_EMAIL | Yes | website, worker, Docker | Service account e-mail |
FIREBASE_PRIVATE_KEY | Yes | website, worker, Docker | Service account private key |
| Secrets | |||
MEDIA_SIGNING_SECRET | Yes | website, Docker | Signs media URLs (openssl rand -hex 24) |
REVENUECAT_WEBHOOK_SECRET | — | website, Docker | Authorization header RevenueCat sends to the webhook |
| Payments | |||
REVENUECAT_API_KEY | — | website, Docker | RevenueCat secret API key (in-app purchases) |
STRIPE_SECRET_KEY | — | website, Docker | Stripe secret key (web checkout) |
STRIPE_WEBHOOK_SECRET | — | website, Docker | Stripe webhook signing secret |
PAYPAL_CLIENT_ID | — | website, Docker | PayPal REST app client id (web checkout) |
PAYPAL_CLIENT_SECRET | — | website, Docker | PayPal REST app secret |
PAYPAL_MODE | — | website, Docker | "live" for real money, anything else = sandbox |
RAZORPAY_KEY_ID | — | website, Docker | Razorpay key id (web checkout, India) |
RAZORPAY_KEY_SECRET | — | website, Docker | Razorpay key secret |
RAZORPAY_WEBHOOK_SECRET | — | website, Docker | Razorpay webhook secret (payment_link.paid) |
FLUTTERWAVE_SECRET_KEY | — | website, Docker | Flutterwave secret key (web checkout, Africa) |
FLUTTERWAVE_WEBHOOK_HASH | — | website, Docker | Flutterwave webhook secret hash (verif-hash header) |
PAYSTACK_SECRET_KEY | — | website, Docker | Paystack 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, worker | S3-compatible endpoint (empty for AWS S3) |
S3_REGION | — | website, worker, Docker | Bucket region |
S3_BUCKET | Yes | website, worker, Docker | Bucket for lesson videos, thumbnails, files and replays |
S3_ACCESS_KEY_ID | Yes | website, worker, Docker | Storage access key |
S3_SECRET_ACCESS_KEY | Yes | website, worker, Docker | Storage secret key |
S3_PUBLIC_ENDPOINT | — | website, Docker | Storage address phones and browsers use (required with the Docker MinIO) |
S3_PUBLIC_URL | — | website, Docker | Public bucket / CDN base URL (leave empty for a private bucket) |
| Sign in with Apple | |||
APPLE_TEAM_ID | — | website, Docker | Token revocation at account deletion |
APPLE_KEY_ID | — | website, Docker | Token revocation at account deletion |
APPLE_CLIENT_ID | — | website, Docker | iOS bundle id |
APPLE_PRIVATE_KEY_PATH | — | website | Path to the .p8 key (or use APPLE_PRIVATE_KEY) |
APPLE_PRIVATE_KEY | — | website, Docker | The .p8 key contents |
| AI | |||
AI_API_KEY | — | website, worker, Docker | Key for any OpenAI-compatible API: study buddy, quiz drafts, Q&A drafts, transcripts |
AI_BASE_URL | — | website, worker, Docker | API base URL (empty = OpenAI; Gemini, OpenRouter and Groq have compatible URLs) |
AI_MODEL | — | website, Docker | Chat model (default gpt-4o-mini) |
AI_TRANSCRIBE_MODEL | — | worker, Docker | Transcription model (default whisper-1) |
SUBTITLE_PROVIDER | — | website, worker, Docker | "openai" to generate lesson transcripts with Whisper |
| Worker | |||
WORKER_CONCURRENCY | — | worker | Parallel transcodes |
SMTP_HOST | — | website, Docker | SMTP server; empty = console mode |
SMTP_PORT | — | website, Docker | 587 (STARTTLS) or 465 (TLS) |
SMTP_USER | — | website, Docker | SMTP user |
SMTP_PASS | — | website, Docker | SMTP password |
SMTP_SECURE | — | website, Docker | "true" for port 465 |
| Mobile app | |||
API_BASE_URL | Yes | app | Your server; the app calls <url>/api/v1 |
APP_NAME | — | app | App name in the UI |
APP_STORE_ID | — | app | Rate-us and share links |
REVENUECAT_IOS_KEY | — | app | RevenueCat public iOS SDK key |
REVENUECAT_ANDROID_KEY | — | app | RevenueCat public Android SDK key |
USE_REVENUE_CAT | — | app | Use store purchases (false = demo) |
LINK_PRIVACY | — | app | Privacy policy URL |
LINK_TERMS | — | app | Terms URL |
LINK_SUPPORT | — | app | Support URL |
LINK_DELETE_ACCOUNT | — | app | Account deletion URL |
SUPPORT_EMAIL | — | app | Support e-mail |
FIREBASE_MESSAGING_SENDER_ID | Yes | app | Firebase config |
FIREBASE_STORAGE_BUCKET | — | app | Firebase config |
FIREBASE_WEB_API_KEY | — | app | Firebase web config (Flutter web) |
FIREBASE_WEB_APP_ID | — | app | Firebase web config (Flutter web) |
FIREBASE_WEB_AUTH_DOMAIN | — | app | Firebase web config (Flutter web) |
FIREBASE_ANDROID_API_KEY | Yes | app | Firebase Android config |
FIREBASE_ANDROID_APP_ID | Yes | app | Firebase Android config |
FIREBASE_IOS_API_KEY | Yes | app | Firebase iOS config |
FIREBASE_IOS_APP_ID | Yes | app | Firebase iOS config |
FIREBASE_IOS_CLIENT_ID | — | app | Google sign-in on iOS |
FIREBASE_IOS_BUNDLE_ID | — | app | iOS bundle id |
| Store tooling | |||
APPLE_APP_ID | — | app | fastlane: App Store app id |
ANDROID_APP_ID | — | app | fastlane: Android package |
APPLE_ID | — | app | fastlane: Apple account |
APPLE_ITC_TEAM_ID | — | app | fastlane: App Store Connect team |
APPLE_DEV_TEAM_ID | — | app | fastlane: 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(...).
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.
| Method | Path | Auth | What it does |
|---|---|---|---|
| System, sign-in and help | |||
GET | /api/v1/health | No | Health check: the database answers and the app version |
GET | /api/v1/config | No | Remote config every client reads on start (minimum versions, paywall, AI limits, goals) |
POST | /api/v1/auth/session | No | Exchange a Firebase ID token for the website's session cookie |
DELETE | /api/v1/auth/session | No | Sign out of the website (clears the session cookie) |
POST | /api/v1/auth/demo | No | Live demo only (DEMO_MODE=true; 404 otherwise, so buyers never ship it on) |
GET | /api/v1/pages/:slug | No | CMS pages (terms, privacy, refund policy, about) for the apps and the website |
GET | /api/v1/help | No | Help centre articles and FAQs, with search and a category filter |
GET | /api/v1/media/:a/:b | Signed link (expires) | Signed media |
GET | /api/v1/media/:a/:b/:c | Signed link (expires) | Signed media |
GET | /api/v1/media/:a/:b/:c/:d | Signed link (expires) | Signed media |
GET | /api/v1/media/:a/:b/:c/:d/:e | Signed link (expires) | Signed media |
| Account | |||
GET | /api/v1/me | Signed in | Signing in again (every client calls GET /me after sign-in) cancels a scheduled deletion |
POST | /api/v1/me/avatar | Signed in | "Change photo": presigned PUT into storage; then PATCH /me { avatarUrl: fileUrl } |
PATCH | /api/v1/me | Signed in | Update your name, photo, bio or learning goal |
GET | /api/v1/me/preferences | Signed in | Your notification and learning preferences |
PATCH | /api/v1/me/preferences | Signed in | Onboarding (interests, goal), appearance, learning, notifications |
POST | /api/v1/me/devices | Signed in | Register this device for push notifications |
DELETE | /api/v1/me/devices/:token | Signed in | Stop push notifications to a device |
POST | /api/v1/me/merge/preview | Signed in | Compares the guest's progress with the account's; the app then calls POST /me/merge |
POST | /api/v1/me/merge | Signed in | "Merge both" keeps the higher streak and every course, note and XP; "Keep account" drops the guest's progress |
POST | /api/v1/me/export | Signed in | A download link (one hour) for a copy of all your data as JSON |
DELETE | /api/v1/me | Signed in | Real accounts are scheduled for deletion in 30 days (signing in again cancels it); guests are deleted now |
POST | /api/v1/me/apple/revoke | Signed in | Sign in with Apple revocation at account deletion (App Store rule) |
| Catalogue, paths, wishlist and blog | |||
GET | /api/v1/home | Optional | The Today screen: continue card, goal, the last seven days, banners, live sessions and shelves |
GET | /api/v1/categories | No | Subjects with their colours and course counts |
GET | /api/v1/courses | No | Search and filter the catalogue (subject, level, price, length, rating, cohorts), paged |
GET | /api/v1/search/suggest | No | Popular searches (Admin → Settings) and live suggestions for what is typed |
GET | /api/v1/instructors | No | Instructors, most followed first |
GET | /api/v1/instructors/:slug | Optional | An instructor's profile and courses |
PUT | /api/v1/instructors/:id/follow | Signed in | Follow an instructor |
DELETE | /api/v1/instructors/:id/follow | Signed in | Unfollow an instructor |
GET | /api/v1/site/stats | No | Catalogue numbers for the website (courses, enrolments, rating, teachers) |
GET | /api/v1/site/testimonials | No | Five-star reviews picked for the website |
GET | /api/v1/courses/:id | Optional | A course page: curriculum, instructor, reviews summary, cohort seats and what you own |
GET | /api/v1/courses/:id/reviews | No | A course's reviews, newest or most helpful first |
PUT | /api/v1/courses/:id/review | Signed in | Write review: enrolled learners only, one review each (posting again edits it) |
POST | /api/v1/courses/:id/enroll | Signed in | Free courses and Plus courses: enroll without an order |
PUT | /api/v1/courses/:id/waitlist | Signed in | Join the waitlist of a full cohort; position = order of joining |
DELETE | /api/v1/courses/:id/waitlist | Signed in | Leave a full cohort's waitlist |
GET | /api/v1/paths | No | Learning paths |
GET | /api/v1/paths/:slug | Optional | A learning path with its courses in order and the bundle price |
GET | /api/v1/wishlist | Signed in | Your wishlist |
PUT | /api/v1/wishlist/:courseId | Signed in | Add a course to your wishlist |
DELETE | /api/v1/wishlist/:courseId | Signed in | Remove a course from your wishlist |
GET | /api/v1/blog | No | Published blog posts |
GET | /api/v1/blog/:slug | No | A blog post |
| Learning: player, notes and Q&A | |||
GET | /api/v1/my/courses | Signed in | Your library: in progress, completed, cohorts or archived |
PATCH | /api/v1/my/courses/:courseId | Signed in | Archive or restore a course in your library |
GET | /api/v1/learn/:courseId | Optional | Player bootstrap: the course, its curriculum with locks and ticks, and where to resume |
GET | /api/v1/lessons/:id | Optional | A lesson to play: signed video links, files, your position and the next lesson |
GET | /api/v1/lessons/:id/transcript | Optional | A lesson's transcript with timestamps |
GET | /api/v1/lessons/:id/download | Signed in | Offline downloads: a signed MP4 of the chosen quality, for owned or Plus courses |
POST | /api/v1/lessons/:id/progress | Signed in | Watch 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/complete | Signed in | "Mark complete" and article lessons |
GET | /api/v1/lessons/:id/notes | Signed in | Your notes on a lesson |
GET | /api/v1/courses/:id/notes | Signed in | All your notes in a course |
POST | /api/v1/lessons/:id/notes | Signed in | Add a note at a moment in the video |
PATCH | /api/v1/notes/:id | Signed in | Edit one of your notes |
DELETE | /api/v1/notes/:id | Signed in | Delete one of your notes |
GET | /api/v1/lessons/:id/questions | No | Questions asked on a lesson |
POST | /api/v1/lessons/:id/questions | Signed in | Ask the instructor a question on a lesson |
GET | /api/v1/questions/:id | No | A question with its answers |
POST | /api/v1/questions/:id/answers | Signed in | Answer a question (learners in the course and its instructor) |
| Quizzes, assignments and uploads | |||
GET | /api/v1/quizzes/:id | Signed in | A quiz and your open attempt |
POST | /api/v1/quizzes/:id/attempts | Signed in | Starts an attempt, or resumes the open one (answers save as you go) |
PUT | /api/v1/attempts/:id/answers | Signed in | Save answers on an open quiz attempt |
POST | /api/v1/attempts/:id/submit | Signed in | Submit a quiz attempt; the server marks it |
GET | /api/v1/attempts/:id | Signed in | A marked attempt with the right answers and explanations |
POST | /api/v1/uploads | Signed in | An upload link for an assignment file or a profile photo (type and size checked first) |
GET | /api/v1/assignments/:id | Signed in | An assignment brief and your submission |
PUT | /api/v1/assignments/:id/submission | Signed in | Hand in or update your assignment |
| AI study buddy | |||
GET | /api/v1/ai/usage | Signed in | Questions left today on the AI study buddy |
POST | /api/v1/ai/ask | Signed in | Ask the AI study buddy about a lesson (counts toward the daily limit) |
POST | /api/v1/ai/summary | Signed in | AI summary of a lesson |
POST | /api/v1/ai/quiz | Signed in | AI practice questions for a lesson |
| Live classes, progress, certificates, notifications | |||
GET | /api/v1/live | Optional | Upcoming live sessions in your cohorts, and replays |
GET | /api/v1/live/:id | Optional | A live session: time, join link when it opens, replay |
PUT | /api/v1/live/:id/reminder | Signed in | Remind me before a live session |
DELETE | /api/v1/live/:id/reminder | Signed in | Cancel a live session reminder |
POST | /api/v1/live/:id/join | Signed in | Waiting room → "Join": the meeting link opens from 10 minutes before the start until the end |
GET | /api/v1/live/:id/calendar.ics | No | A live session as a calendar file |
GET | /api/v1/cohorts/:courseId/calendar.ics | No | "Add all to calendar": every session of a cohort |
GET | /api/v1/cohorts/:courseId/hub | Signed in | Cohort hub: week, next session, sessions done, this week's items, discussion, classmates |
GET | /api/v1/progress | Signed in | Your streak, XP, weekly minutes and goal history |
POST | /api/v1/progress/freeze | Signed in | Use a streak freeze for a missed day |
GET | /api/v1/badges | Signed in | Badges you earned and the ones still locked |
GET | /api/v1/leaderboard | Signed in | Weekly / monthly XP from the XP ledger; "classmates" = learners who share a course with you |
GET | /api/v1/certificates | Signed in | Your certificates |
GET | /api/v1/verify/:code | No | Public: anyone can check a certificate id (QR on the PDF) |
GET | /api/v1/certificates/:code/pdf | No | Download a certificate as a PDF |
GET | /api/v1/notifications | Signed in | Your notifications |
POST | /api/v1/notifications/read | Signed in | Mark notifications as read |
DELETE | /api/v1/notifications/:id | Signed in | Delete a notification |
POST | /api/v1/support | Optional | Send a message to support |
POST | /api/v1/feedback | Optional | Send feedback about the app |
POST | /api/v1/newsletter | No | Newsletter box: signing up twice is fine, and signing up again after unsubscribing turns it back on |
POST | /api/v1/teach/apply | Signed in | Apply to teach |
GET | /api/v1/teach/application | Signed in | The status of your application to teach |
| Cart, checkout, orders and wallet | |||
GET | /api/v1/cart | Signed in | Your cart with prices, discounts and tax |
POST | /api/v1/cart | Signed in | Add a course or path to your cart |
DELETE | /api/v1/cart/:itemId | Signed in | Remove an item from your cart |
POST | /api/v1/cart/quote | Signed in | Coupon, wallet and tax preview for the cart or a "Buy now" item |
GET | /api/v1/checkout/methods | Optional | The payment methods this server can take right now |
POST | /api/v1/checkout | Signed in | Pays for the cart (or one course/path) |
POST | /api/v1/checkout/confirm | Signed in | PayPal, Razorpay, Paystack and Flutterwave hand the buyer back with a query; verify with the gateway and settle once |
GET | /api/v1/orders | Signed in | Your orders |
GET | /api/v1/orders/:code | Signed in | One of your orders |
GET | /api/v1/orders/:code/invoice | Signed in | Download an order's invoice as a PDF |
POST | /api/v1/orders/:code/refund | Signed in | Ask for a refund within the refund window |
POST | /api/v1/orders/:code/proof | Signed in | "Upload proof of payment": attaches the receipt to a bank-transfer order for the admin |
POST | /api/v1/orders/:code/cancel | Signed in | Cancel an unpaid order (gives a coupon use back) |
GET | /api/v1/wallet | Signed in | Your wallet balance and history |
POST | /api/v1/wallet/topup | Signed in | Start a wallet top-up through a payment gateway |
POST | /api/v1/wallet/redeem | Signed in | Redeem a gift card into your wallet |
| Learnova Plus and payment webhooks | |||
GET | /api/v1/plans | No | Paywall: plans with prices from the server; the app swaps in the store's localised price when it has one |
GET | /api/v1/plus | Signed in | Manage subscription with its billing history |
POST | /api/v1/purchases/sync | Signed in | Asks RevenueCat for the subscriber and applies anything not credited yet (restore) |
POST | /api/v1/purchases/demo | Signed in | DEMO mode only: grants exactly what the store webhook would, without payment (store "demo") |
POST | /api/v1/plus/checkout | Signed in | Website Plus: Stripe subscription, or a one-period pass through the other gateways |
POST | /api/v1/plus/portal | Signed in | Open the Stripe billing portal for a website Plus subscription |
POST | /api/v1/webhooks/revenuecat | Signed by the provider | RevenueCat events for app subscriptions (shared secret) |
POST | /api/v1/webhooks/razorpay | Signed by the provider | Razorpay payment events (signature checked) |
POST | /api/v1/webhooks/paystack | Signed by the provider | Paystack payment events (signature checked) |
POST | /api/v1/webhooks/flutterwave | Signed by the provider | Flutterwave payment events (verification hash checked) |
POST | /api/v1/webhooks/stripe | Signed by the provider | Stripe events: checkout paid, subscription renewed or ended (signature checked) |
| Instructor: courses and curriculum | |||
GET | /api/v1/instructor/me | Instructor | Your instructor profile and counters for the studio |
GET | /api/v1/instructor/dashboard | Instructor | Studio dashboard: revenue, students, ratings and what needs you |
GET | /api/v1/instructor/courses | Instructor | Your courses with status and sales |
POST | /api/v1/instructor/courses | Instructor | Start a new course (draft) |
GET | /api/v1/instructor/courses/:id | Instructor | Builder: details, curriculum with every lesson's state, pricing, checklist, review notes |
PATCH | /api/v1/instructor/courses/:id | Instructor | Edit a course's details, thumbnail and promo video |
PUT | /api/v1/instructor/courses/:id/pricing | Instructor | Set a course's price, sale and Plus inclusion |
DELETE | /api/v1/instructor/courses/:id | Instructor | Delete a draft course |
POST | /api/v1/instructor/courses/:id/unpublish | Instructor | Unpublish a course (owners keep access) |
POST | /api/v1/instructor/courses/:id/publish | Instructor | Publish an approved course that is waiting unlisted |
POST | /api/v1/instructor/courses/:id/submit | Instructor | Submit for review (or publish straight away when Admin → Settings auto-approves courses) |
POST | /api/v1/instructor/courses/:id/sections | Instructor | Add a section |
PATCH | /api/v1/instructor/sections/:id | Instructor | Rename a section |
DELETE | /api/v1/instructor/sections/:id | Instructor | Delete a section and its lessons |
PUT | /api/v1/instructor/courses/:id/sections/order | Instructor | Save the order of sections and lessons |
POST | /api/v1/instructor/courses/:id/lessons | Instructor | Add a lesson (video, article, quiz, assignment or live) |
GET | /api/v1/instructor/lessons/:id | Instructor | A lesson in the editor |
PATCH | /api/v1/instructor/lessons/:id | Instructor | Edit a lesson |
DELETE | /api/v1/instructor/lessons/:id | Instructor | Delete a lesson |
PUT | /api/v1/instructor/sections/:id/lessons/order | Instructor | Drag and drop inside a section, or into another section: `ids` is the section's new order |
POST | /api/v1/instructor/lessons/:id/files | Instructor | Attach a downloadable file to a lesson |
DELETE | /api/v1/instructor/files/:id | Instructor | Remove a lesson file |
POST | /api/v1/instructor/uploads | Instructor | Get an upload link for a video, thumbnail or file |
POST | /api/v1/instructor/uploads/:id/complete | Instructor | Confirm an upload; videos go to the worker for transcoding |
POST | /api/v1/instructor/uploads/:id/failed | Instructor | "Upload failed": the client marks a dropped upload so the lesson shows the resume state |
POST | /api/v1/instructor/lessons/:id/transcript | Instructor | Instructor captions: generate a transcript from the video (needs SUBTITLE_PROVIDER), "Auto captions" |
GET | /api/v1/instructor/lessons/:id/quiz | Instructor | A quiz in the editor |
PUT | /api/v1/instructor/lessons/:id/quiz | Instructor | Edit a quiz's settings (pass mark, attempts, time limit) |
POST | /api/v1/instructor/lessons/:id/quiz/questions | Instructor | Add a quiz question |
PUT | /api/v1/instructor/questions/:id | Instructor | Edit a quiz question |
DELETE | /api/v1/instructor/questions/:id | Instructor | Delete a quiz question |
PUT | /api/v1/instructor/lessons/:id/quiz/order | Instructor | Save the order of quiz questions |
POST | /api/v1/instructor/ai/quiz-draft | Instructor | "Generate with AI": drafts questions from a lesson's transcript; the instructor reviews before saving |
GET | /api/v1/instructor/lessons/:id/assignment | Instructor | An assignment in the editor |
PUT | /api/v1/instructor/lessons/:id/assignment | Instructor | Edit an assignment's brief, due date and marking |
| Instructor: students, Q&A, live, coupons, earnings | |||
GET | /api/v1/instructor/coupons | Instructor | Your coupons |
POST | /api/v1/instructor/coupons | Instructor | Create a coupon for your courses |
PUT | /api/v1/instructor/coupons/:id | Instructor | Edit one of your coupons |
DELETE | /api/v1/instructor/coupons/:id | Instructor | Delete one of your coupons |
GET | /api/v1/instructor/students | Instructor | Learners in your courses with progress |
GET | /api/v1/instructor/students/export.csv | Instructor | Your learners as a CSV file |
GET | /api/v1/instructor/students/:enrollmentId | Instructor | One learner's progress, quiz scores and submissions |
GET | /api/v1/instructor/questions | Instructor | Questions waiting for you |
GET | /api/v1/instructor/questions/:id | Instructor | A question thread |
POST | /api/v1/instructor/questions/:id/draft | Instructor | "AI draft from your lesson transcript": a suggestion only; the instructor edits and posts it |
POST | /api/v1/instructor/questions/:id/answers | Instructor | Answer a question |
POST | /api/v1/instructor/questions/:id/hide | Instructor | Hide a question |
GET | /api/v1/instructor/submissions | Instructor | Assignments waiting to be marked |
GET | /api/v1/instructor/submissions/:id | Instructor | A submission with its files |
POST | /api/v1/instructor/submissions/:id/grade | Instructor | Mark a submission and leave feedback |
GET | /api/v1/instructor/announcements | Instructor | Your announcements |
POST | /api/v1/instructor/announcements | Instructor | Send an announcement to a course's learners |
GET | /api/v1/instructor/live | Instructor | Your live sessions |
POST | /api/v1/instructor/live | Instructor | Schedule a live session (Zoom, Meet or Teams link) |
PATCH | /api/v1/instructor/live/:id | Instructor | Edit a live session or add its replay |
DELETE | /api/v1/instructor/live/:id | Instructor | Cancel a live session |
GET | /api/v1/instructor/earnings | Instructor | Your earnings: course sales, Plus minutes, refunds |
GET | /api/v1/instructor/earnings/export.csv | Instructor | Your earnings as a CSV file |
GET | /api/v1/instructor/payouts | Instructor | Your payouts and available balance |
POST | /api/v1/instructor/payouts | Instructor | Request a payout of your available balance |
GET | /api/v1/instructor/settings | Instructor | Your payout method and studio settings |
PATCH | /api/v1/instructor/settings | Instructor | Update your payout method and studio settings |
GET | /api/v1/instructor/reviews | Instructor | Reviews on your courses |
PUT | /api/v1/instructor/reviews/:id/reply | Instructor | Reply to a review |
GET | /api/v1/instructor/analytics | Instructor | Views, enrolments, completion and watch time per course |
| Admin: catalogue, people and money | |||
GET | /api/v1/admin/me | Staff: courses · view | The signed-in staff member and what their role may do |
GET | /api/v1/admin/dashboard | Staff: reports · view | Admin dashboard: month figures, revenue chart, review queue, latest orders |
GET | /api/v1/admin/courses | Staff: courses · view | All courses, by status (in review first) |
GET | /api/v1/admin/courses/:id | Staff: courses · view | A course with its review checklist |
POST | /api/v1/admin/courses/:id/scan | Staff: courses · view | "AI moderation scan": the course's text checked by the configured model |
POST | /api/v1/admin/courses/:id/approve | Staff: courses · approve | Approve a course in review (publishes it) |
POST | /api/v1/admin/courses/:id/request-changes | Staff: courses · approve | Send a course back to its instructor with notes |
PATCH | /api/v1/admin/courses/:id | Staff: courses · edit | Change a course's status (published, unlisted, archived) or badges |
GET | /api/v1/admin/courses-export | Staff: courses · view | All courses as a CSV file |
GET | /api/v1/admin/categories | Staff: categories · view | Subjects |
POST | /api/v1/admin/categories | Staff: categories · create | Create a subject |
PATCH | /api/v1/admin/categories/:id | Staff: categories · edit | Edit a subject |
PUT | /api/v1/admin/categories/order | Staff: categories · edit | Save the order of subjects |
POST | /api/v1/admin/categories/:id/delete | Staff: categories · delete | A category with courses can only go once its courses move to another one |
GET | /api/v1/admin/instructors | Staff: instructors · view | Instructors and applications |
GET | /api/v1/admin/instructors/:id | Staff: instructors · view | An instructor or application |
POST | /api/v1/admin/instructors/:id/approve | Staff: instructors · approve | Approve an application to teach |
POST | /api/v1/admin/instructors/:id/reject | Staff: instructors · approve | Reject an application to teach |
PATCH | /api/v1/admin/instructors/:id | Staff: instructors · edit | Edit an instructor's profile or revenue share |
GET | /api/v1/admin/students | Staff: students · view | Learners |
GET | /api/v1/admin/students-export | Staff: students · view | Learners as a CSV file |
GET | /api/v1/admin/students/:id | Staff: students · view | A learner's courses, orders and activity |
POST | /api/v1/admin/students/:id/suspend | Staff: students · edit | Suspend a learner |
POST | /api/v1/admin/students/:id/unsuspend | Staff: students · edit | Lift a learner's suspension |
POST | /api/v1/admin/students/:id/enroll | Staff: students · edit | Enrol a learner in a course for free |
GET | /api/v1/admin/orders | Staff: orders · view | Orders, filtered by status, gateway and date |
GET | /api/v1/admin/orders-export | Staff: orders · view | Orders as a CSV file |
GET | /api/v1/admin/orders/:code | Staff: orders · view | An order with its payments and refunds |
POST | /api/v1/admin/orders/:code/refund | Staff: orders · approve | Refund an order (all or part) |
GET | /api/v1/admin/refunds | Staff: orders · view | Refund requests |
POST | /api/v1/admin/refunds/:id/decide | Staff: orders · approve | Approve or decline a refund request |
POST | /api/v1/admin/orders/:code/bank | Staff: orders · approve | Bank transfer (orders "Pending"): approve once the money arrived, or decline (wallet money comes back) |
POST | /api/v1/admin/orders/:code/receipt | Staff: orders · view | Confirm a bank transfer and fulfil the order |
GET | /api/v1/admin/payouts | Staff: payouts · view | Instructor payout requests |
POST | /api/v1/admin/payouts/approve | Staff: payouts · approve | Approves the selected requests; payouts on hold stay on hold |
POST | /api/v1/admin/payouts/:id/action | Staff: payouts · approve | Mark a payout paid, hold, release or reject it |
GET | /api/v1/admin/coupons | Staff: coupons · view | Coupons |
POST | /api/v1/admin/coupons | Staff: coupons · create | Platform coupons: the platform pays the discount, so instructors keep their share of the full price |
PUT | /api/v1/admin/coupons/:id | Staff: coupons · edit | Edit a coupon |
DELETE | /api/v1/admin/coupons/:id | Staff: coupons · delete | Delete a coupon |
GET | /api/v1/admin/plans | Staff: settings · view | Plus plans |
POST | /api/v1/admin/plans | Staff: settings · create | Create a Plus plan |
PATCH | /api/v1/admin/plans/:id | Staff: settings · edit | Edit a Plus plan |
GET | /api/v1/admin/reviews | Staff: reviews · view | Flagged reviews |
POST | /api/v1/admin/reviews/:id/keep | Staff: reviews · edit | Keep a flagged review |
POST | /api/v1/admin/reviews/:id/remove | Staff: reviews · delete | Remove a review |
| Admin: content, settings and staff | |||
GET | /api/v1/admin/certificate-templates | Staff: certificates · view | Certificate templates |
POST | /api/v1/admin/certificate-templates | Staff: certificates · create | Create a certificate template |
PUT | /api/v1/admin/certificate-templates/:id | Staff: certificates · edit | Edit a certificate template |
DELETE | /api/v1/admin/certificate-templates/:id | Staff: certificates · delete | Delete a certificate template |
POST | /api/v1/admin/certificate-templates/preview | Staff: certificates · view | "Preview PDF": the template with sample data, before or after saving |
POST | /api/v1/admin/certificates/:code/revoke | Staff: certificates · edit | Revoke a certificate (its public link says so) |
GET | /api/v1/admin/blog | Staff: blog · view | Blog posts |
GET | /api/v1/admin/blog/:id | Staff: blog · view | A blog post in the editor |
POST | /api/v1/admin/blog | Staff: blog · create | Create a blog post |
PUT | /api/v1/admin/blog/:id | Staff: blog · edit | Edit a blog post |
DELETE | /api/v1/admin/blog/:id | Staff: blog · delete | Delete a blog post |
GET | /api/v1/admin/banners | Staff: banners · view | Home banners |
POST | /api/v1/admin/banners | Staff: banners · create | Create a home banner |
PUT | /api/v1/admin/banners/:id | Staff: banners · edit | Edit a home banner |
PUT | /api/v1/admin/banners/order | Staff: banners · edit | Save the order of home banners |
DELETE | /api/v1/admin/banners/:id | Staff: banners · delete | Delete a home banner |
GET | /api/v1/admin/push | Staff: banners · view | Push campaigns |
POST | /api/v1/admin/push/estimate | Staff: banners · view | Count the devices a push campaign would reach |
POST | /api/v1/admin/push | Staff: banners · create | Send or schedule a push campaign |
POST | /api/v1/admin/push/test | Staff: 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/:id | Staff: banners · delete | Cancel a scheduled push campaign |
GET | /api/v1/admin/reports | Staff: reports · view | Reports: revenue, enrolments, top courses and instructors |
GET | /api/v1/admin/reports/export.csv | Staff: reports · view | Reports as a CSV file |
GET | /api/v1/admin/settings/:group | Staff: settings · view | One settings group (app, payments, email, AI, live, …) |
PUT | /api/v1/admin/settings/:group | Staff: settings · edit | Save one settings group |
GET | /api/v1/admin/settings-status | Staff: settings · view | Which keys the server has (never their values) and each gateway's on/off switch |
POST | /api/v1/admin/settings/test-email | Staff: settings · edit | Send a test email with the saved mail settings |
GET | /api/v1/admin/tax-rules | Staff: settings · view | Tax rules by country |
POST | /api/v1/admin/tax-rules | Staff: settings · create | Create a tax rule |
PUT | /api/v1/admin/tax-rules/:id | Staff: settings · edit | Edit a tax rule |
DELETE | /api/v1/admin/tax-rules/:id | Staff: settings · delete | Delete a tax rule |
GET | /api/v1/admin/languages | Staff: settings · view | Course languages |
POST | /api/v1/admin/languages | Staff: settings · create | Add a course language |
PATCH | /api/v1/admin/languages/:code | Staff: settings · edit | Turn a course language on or off |
GET | /api/v1/admin/pages | Staff: blog · view | Legal and info pages |
GET | /api/v1/admin/pages/:slug | Staff: blog · view | A page in the editor |
PUT | /api/v1/admin/pages/:slug | Staff: blog · edit | Save a page |
GET | /api/v1/admin/faqs | Staff: blog · view | Help centre FAQs |
POST | /api/v1/admin/faqs | Staff: blog · create | Create a FAQ |
PUT | /api/v1/admin/faqs/:id | Staff: blog · edit | Edit a FAQ |
DELETE | /api/v1/admin/faqs/:id | Staff: blog · delete | Delete a FAQ |
GET | /api/v1/admin/gift-cards | Staff: orders · view | Gift cards |
POST | /api/v1/admin/gift-cards | Staff: orders · create | Create gift cards |
GET | /api/v1/admin/staff | Staff: staff · view | Staff members and roles |
POST | /api/v1/admin/staff/invite | Staff: staff · create | Invite a staff member by email |
POST | /api/v1/admin/staff/:id/resend | Staff: staff · create | Resend a staff invitation |
PATCH | /api/v1/admin/staff/:id | Staff: staff · edit | Change a staff member's role or turn their access off (super admin) |
POST | /api/v1/admin/staff/:id/reset-two-step | Staff: 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/:id | Staff: staff · delete | Remove a staff member |
POST | /api/v1/admin/roles | Staff: staff · create | Create a role with its permission grid |
PUT | /api/v1/admin/roles/:code | Staff: staff · edit | Edit a role's permissions |
DELETE | /api/v1/admin/roles/:code | Staff: staff · delete | Delete a role |
GET | /api/v1/admin/audit | Staff: staff · view | The audit log of staff actions |
| Admin: setup check | |||
GET | /api/v1/admin/setup | Staff: settings · view | Setup check: which keys are set and what each one turns on |
POST | /api/v1/admin/setup/test-email | Staff: settings · edit | Sends a test email to the signed-in staff member (super admin; the demo admin is read-only) |
POST | /api/v1/admin/auth/password-reset | No | Always 202 so the form never reveals who has an account |
GET | /api/v1/admin/auth/invite/:token | No | Turns 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:
| Table | What it holds | Keys | Points to |
|---|---|---|---|
| People | |||
users | Everyone 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_roles | Admin roles. permissions = { <area>: ["view","create","edit","delete","approve"] }. | PK code | — |
devices | Push tokens for a user's phones and browsers. | PK id unique token | user_id → users |
instructors | An instructor profile; created by the Teach application, approved by admin. | PK id unique owner_id unique slug | owner_id → users |
| Catalogue | |||
categories | Subjects in the catalogue, with their colour and icon. | PK id unique slug | — |
courses | A course or live cohort: details, price, status, cohort dates and seat limit. | PK id unique slug | category_id → categories instructor_id → instructors |
sections | A course's sections, in order. | PK id | course_id → courses |
lessons | A lesson in a section: video, article, quiz, assignment or live, with its video keys and preview flag. | PK id | course_id → courses section_id → sections |
lesson_files | Lesson resources: an uploaded file (key) or a link (url). | PK id | lesson_id → lessons |
quizzes | A quiz lesson's settings: pass mark, attempts and time limit. | PK id unique lesson_id | lesson_id → lessons |
quiz_questions | A quiz question with its choices and the right answer (never sent to the player before marking). | PK id | quiz_id → quizzes |
quiz_attempts | A learner's attempt at a quiz, with answers and the server's mark. | PK id | quiz_id → quizzes user_id → users |
assignments | An assignment lesson's brief, due date and points. | PK id unique lesson_id | lesson_id → lessons |
submissions | A 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_paths | A learning path: several courses in order, sold as a bundle. | PK id unique slug | — |
learning_path_courses | The courses in a learning path, in order. | PK (path_id, course_id) | path_id → learning_paths course_id → courses |
| Learning | |||
enrollments | A 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_progress | Where a learner is in a lesson and whether it is complete. | PK (user_id, lesson_id) | user_id → users lesson_id → lessons |
watch_days | Seconds watched per learner per course per day; feeds streaks, the weekly chart, analytics and the Plus pool. | PK (user_id, course_id, day) | — |
notes | A learner's note at a moment in a lesson. | PK id | user_id → users course_id → courses lesson_id → lessons |
questions | A question asked on a lesson (Q&A). | PK id | user_id → users course_id → courses lesson_id → lessons |
answers | An answer to a question, by a learner or the instructor. | PK id | question_id → questions user_id → users |
reviews | A 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_items | Courses a learner saved for later. | PK (user_id, course_id) | user_id → users course_id → courses |
cart_items | Courses 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 |
certificates | A 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_templates | Certificate designs the admin can choose from. | PK id | — |
| Gamification | |||
xp_events | Every XP grant, idempotent per (user, reason, ref). Weekly / monthly leaderboards sum these. | PK id unique (user_id, reason, ref_id) | user_id → users |
daily_activity | Minutes learned per learner per day, the goal and streak freezes. | PK (user_id, day) | user_id → users |
badges | Badges learners can earn and their rules. | PK code | — |
user_badges | Badges a learner has earned. | PK (user_id, badge_code) | user_id → users badge_code → badges |
| Live & cohorts | |||
live_sessions | A 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_reminders | A learner's reminder for a live session. | PK (user_id, session_id) | user_id → users session_id → live_sessions |
waitlist_entries | A 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 | |||
coupons | Discount codes: percent or fixed, limits and dates (uses are counted atomically). | PK id unique code | instructor_id → instructors |
orders | A purchase: lines, totals, tax, coupon, gateway and status. | PK id unique code unique external_id unique idempotency_key | user_id → users |
order_items | The courses, paths, plans or top-ups in an order. | PK id | order_id → orders course_id → courses path_id → learning_paths instructor_id → instructors |
refunds | Refund requests and refunds on orders. | PK id | order_id → orders order_item_id → order_items user_id → users |
wallets | A learner's wallet balance (cents, one currency). | PK user_id | user_id → users |
wallet_transactions | Every wallet credit and debit, with the order or gift card behind it. | PK id unique idempotency_key | user_id → users |
gift_cards | Gift cards and their redemption. | PK id unique code | — |
plans | Learnova Plus plans. Teams is a display-only plan ("contact sales") in release 1. | PK id unique store_product_id | — |
subscriptions | Learnova Plus subscriptions from the stores (RevenueCat) and the website (Stripe). | PK id unique external_id | user_id → users plan_id → plans |
tax_rules | Tax rates by country (and region). | PK id unique (country, region) | — |
earning_entries | Instructor earnings ledger: sales (70 %), Plus minutes, cohort seats, refunds. Available after the hold. | PK id unique ref | instructor_id → instructors |
payouts | Instructor payout requests and their status. | PK id unique code | instructor_id → instructors |
| Engagement, content, system | |||
notifications | In-app notifications for a user. | PK id | user_id → users |
announcements | Instructor announcement to a course's learners: inbox + push, 2 per course per month. | PK id | instructor_id → instructors course_id → courses |
push_campaigns | Push campaigns sent or scheduled from the admin. | PK id | — |
banners | Home banners in the app and on the website. | PK id | — |
blog_posts | Blog posts. | PK id unique slug | — |
cms_pages | Legal and info pages (terms, privacy, refund policy, about). | PK slug | — |
faqs | Help centre questions and answers. | PK id | — |
languages | Course languages that can be picked in the studio. | PK code | — |
settings | Key/value remote config, grouped (app, learning, payments, plus, ai, live, email, storage). | PK (group, key) | — |
support_messages | Messages sent to support and feedback from the apps and website. | PK id unique ticket | user_id → users |
ai_messages | Questions to the AI study buddy and its answers (counts the daily limit). | PK id | user_id → users |
uploads | Files on their way to storage: who uploads what, type, size and status. | PK id unique key | — |
audit_logs | What each staff member changed, and when. | PK id | actor_id → users |
course_daily_stats | Per course per day: page views, enrollments, revenue, watch seconds (instructor analytics, admin reports). | PK (course_id, day) | course_id → courses |
newsletter_subscribers | Blog "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).
| Permission | Where | Text in the kit |
|---|---|---|
| Photo library (iOS) | ios/Runner/Info.plist → NSPhotoLibraryUsageDescription | Choose a photo for your profile or attach an image to an assignment. |
| Notifications | Asked in the app after onboarding | Live-class reminders, replies to your questions, streak reminders. |
| Internet, notifications (Android) | android/app/src/main/AndroidManifest.xml | No text needed. |
Placeholder ids to replace
These ids are placeholders in the download. Replace each with your own before you publish.
| What | Where | Placeholder |
|---|---|---|
| Android application id / iOS bundle id | node tools/rename.mjs --id com.yourcompany.yourapp | com.yourcompany.learnova |
| Firebase apps | flutterfire configure (chapter Firebase setup) | your-firebase-project in google-services.json and GoogleService-Info.plist |
| Plus subscription products | App Store Connect, Google Play Console, then Admin → Plans & Plus | learnova_plus_monthly, learnova_plus_yearly, learnova_plus_student |
| RevenueCat keys | learnova_mobile_flutter/.env | empty (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
- 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. - Create
android/key.properties:
Without this file, release builds are signed with the debug key (fine for testing, refused by Play).storePassword=… keyPassword=… keyAlias=upload storeFile=upload-keystore.jks - Set the version in
pubspec.yaml(version: 1.0.0+1; raise the number after+for every upload), thenflutter build appbundle. - 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 uploadbuild/app/outputs/bundle/release/app-release.aabto a testing track first. - Add the SHA-1/SHA-256 of Play's app signing key (Play Console → Test and release → App integrity) to Firebase.
App Store
- In the Apple Developer site, register your bundle id with Push Notifications, Sign in with Apple and In-App Purchase.
- Open
ios/Runner.xcworkspacein Xcode → Runner → Signing & Capabilities → choose your team (Automatic signing is already on; chapter 9). - In App Store Connect create the app, fill the listing and App Privacy, create the Plus subscriptions (chapter 10).
flutter build ipa, then uploadbuild/ios/ipa/*.ipawith the Transporter app, and send it to TestFlight first.
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.