Learnova

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.

1. 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. Until you add payment keys, purchases run in demo mode (courses and Plus are granted without payment and labelled DEMO). AI, email and push each show a clear "add your key" state until set.

2. Requirements

To run the server

To build the mobile app

Accounts you will create (all have free tiers)

3. 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.

4. Firebase setup

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

  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 20 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.

5. 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.

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).

6. Other hosting (Vercel, cloud storage)

Website on Vercel

  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).

7. 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:

8. 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).

9. 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).
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).

10. Learnova Plus in the apps (RevenueCat)

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.

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. Until any payment key is set (RevenueCat, Stripe, PayPal, Razorpay, Paystack or Flutterwave), purchases are granted without payment and labelled DEMO, so you can try every flow. Adding a key switches to live mode; set DEMO_PURCHASES=false to force live mode.

11. 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).

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.

12. Currency and tax

13. Push notifications

  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).

14. 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.

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

15. 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".

16. 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.

17. Email

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.

18. Sign-in methods

19. 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).

20. 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.

22. 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/

23. 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"false" turns demo purchases off
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
DEMO_ADMIN_PASSWORD—website, DockerPublic demo only: shows a one-click demo button on the admin sign-in
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

24. 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, my learning, 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

25. Publish to the stores

You publish the app under your own developer accounts. Before you start: set your app id (chapter 20), 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.
  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.

26. 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.

27. 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.

28. Credits

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

Outfit and JetBrains Mono, SIL Open Font License 1.1 (licence file in learnova_mobile_flutter/assets/fonts/).

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.

29. Changelog

1.0.0 — 2026-10-07

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

30. 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.