Developer Implementation Guide

Spoken English Class App

Android app with YouTube tutorials, Google Drive materials, Google Forms assignments, and Grok AI sentence correction β€” full stack implementation guide.

Contents

01

App Architecture Overview

πŸ—οΈ System Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ STUDENT ANDROID APP β”‚ β”‚ (Flutter β€” single APK download) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚Tutorials β”‚ β”‚Materials β”‚ β”‚Assignmentsβ”‚ β”‚AI Correctβ”‚ β”‚ β”‚ β”‚(YouTube) β”‚ β”‚(Drive PDF)β”‚ β”‚(G.Forms) β”‚ β”‚(Grok AI) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ YOUR BACKEND API (Node.js) β”‚ β”‚ Hosted on OCI Always Free VM β”‚ β”‚ - Student auth (JWT) β”‚ β”‚ - Tutorial metadata (title, YT URL, thumbnail) β”‚ β”‚ - Material metadata (title, Drive link, category) β”‚ β”‚ - Assignment list (title, GForm URL, due date) β”‚ β”‚ - Grok AI proxy (protects your API key) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MySQL Database β”‚ β”‚ students | tutorials | materials | assignments β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ADMIN PANEL (Web β€” React) β”‚ β”‚ Add/edit YouTube videos, Drive links, GForm URLs β”‚ β”‚ Manage students, view progress β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ“±

Student App

Flutter Android app. Students download APK or install from Play Store. Login with phone + OTP or email/password.

πŸ”§

Your Backend

Node.js + Express API on OCI Free VM. Stores all content metadata. Proxies Grok AI calls to protect your key.

πŸ–₯️

Admin Panel

React web app. Only you access this. Add YouTube links, Drive PDF links, Google Form URLs from browser.

πŸ€–

Grok AI

Students type sentences, backend sends to Grok xAI API, returns corrected sentence + explanation.

02

Tech Stack Decisions

πŸ“¦ Recommended Stack

LayerTechnologyReason
Android AppFlutterYou already use Flutter. Fast UI, YouTube player package available.
Backend APINode.js + ExpressLightweight, easy Grok API proxy, runs well on OCI Free VM.
DatabaseMySQLYou already use MySQL in RM CMS projects.
Admin PanelReact (Vite)Simple CRUD UI, deploy alongside backend.
AuthJWT + bcryptStateless, works for mobile apps well.
HostingOCI Always Free VMZero cost. You already have this setup.
YouTubeyoutube_player_flutterPlays unlisted YouTube videos directly in app. No API key needed for basic playback.
AIGrok xAI APIYou already have the API key.
⚠️ Never put your Grok API key inside the Flutter app. Always call through your backend. If key is in APK, anyone can extract it and use your quota.
03

Database Schema (MySQL)

πŸ—„οΈ Full Schema

-- Students table
CREATE TABLE students (
  id         INT PRIMARY KEY AUTO_INCREMENT,
  name       VARCHAR(100) NOT NULL,
  email      VARCHAR(150) UNIQUE,
  phone      VARCHAR(15) UNIQUE,
  password   VARCHAR(255),      -- bcrypt hash
  batch      VARCHAR(50),       -- e.g. 'Batch A - Morning'
  is_active  TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tutorial videos (YouTube)
CREATE TABLE tutorials (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  title        VARCHAR(200) NOT NULL,
  description  TEXT,
  youtube_url  VARCHAR(500) NOT NULL, -- full URL or video ID
  thumbnail    VARCHAR(500),            -- optional custom thumbnail
  category     VARCHAR(100),           -- e.g. 'Grammar', 'Pronunciation'
  sort_order   INT DEFAULT 0,
  is_published TINYINT DEFAULT 1,
  created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Reading materials (Google Drive links)
CREATE TABLE materials (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  title       VARCHAR(200) NOT NULL,
  description TEXT,
  drive_url   VARCHAR(500) NOT NULL,
  file_type   ENUM('PDF', 'DOC', 'SHEET', 'SLIDE', 'OTHER') DEFAULT 'PDF',
  category    VARCHAR(100),
  sort_order  INT DEFAULT 0,
  is_published TINYINT DEFAULT 1,
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Assignments (Google Forms)
CREATE TABLE assignments (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  title        VARCHAR(200) NOT NULL,
  description  TEXT,
  form_url     VARCHAR(500) NOT NULL,  -- Google Form link
  due_date     DATE,
  max_marks    INT,
  is_published TINYINT DEFAULT 1,
  created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- AI correction history (optional, for your records)
CREATE TABLE ai_corrections (
  id              INT PRIMARY KEY AUTO_INCREMENT,
  student_id      INT,
  original_text   TEXT,
  corrected_text  TEXT,
  created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (student_id) REFERENCES students(id)
);
04

Backend API (Node.js + Express)

πŸ“ Project Structure

spoken-english-api/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   β”œβ”€β”€ auth.js          # login, register
β”‚   β”‚   β”œβ”€β”€ tutorials.js     # GET /tutorials
β”‚   β”‚   β”œβ”€β”€ materials.js     # GET /materials
β”‚   β”‚   β”œβ”€β”€ assignments.js   # GET /assignments
β”‚   β”‚   β”œβ”€β”€ ai.js            # POST /ai/correct
β”‚   β”‚   └── admin.js         # all CRUD for admin panel
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ auth.js          # JWT verify middleware
β”‚   β”‚   └── adminAuth.js     # admin-only middleware
β”‚   β”œβ”€β”€ db.js                # MySQL connection pool
β”‚   └── app.js               # Express app setup
β”œβ”€β”€ .env
β”œβ”€β”€ package.json
└── server.js

βš™οΈ server.js + db.js

// server.js
const app = require('./src/app');
app.listen(3000, () => console.log('API running on port 3000'));

// src/db.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10
});
module.exports = pool;

// .env
DB_HOST=localhost
DB_USER=root
DB_PASS=yourpassword
DB_NAME=spoken_english
JWT_SECRET=your_jwt_secret_here
GROK_API_KEY=your_grok_api_key_here
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=bcrypt_hash_here

πŸ”— API Routes Reference

MethodEndpointAuthDescription
POST/auth/loginNoneStudent login β†’ returns JWT
GET/tutorialsJWTAll published tutorials list
GET/materialsJWTAll published materials list
GET/assignmentsJWTAll published assignments
POST/ai/correctJWTSend sentence β†’ get Grok correction
POST/admin/tutorialsAdminAdd new tutorial
PUT/admin/tutorials/:idAdminEdit tutorial
DELETE/admin/tutorials/:idAdminDelete tutorial
Same CRUD routes for /admin/materials and /admin/assignments
05

Flutter Android App

πŸ“¦ pubspec.yaml β€” Required Packages

dependencies:
  flutter:
    sdk: flutter

  # HTTP
  dio: ^5.4.0
  flutter_secure_storage: ^9.0.0    # store JWT safely

  # YouTube player
  youtube_player_flutter: ^8.1.2    # plays YT videos in-app

  # Open Drive links in browser
  url_launcher: ^6.2.5

  # Open Google Forms in WebView
  webview_flutter: ^4.7.0

  # State management
  provider: ^6.1.2                  # or use Riverpod

  # UI helpers
  cached_network_image: ^3.3.1
  shimmer: ^3.0.0                   # loading skeleton

πŸ“‚ App Folder Structure

lib/
β”œβ”€β”€ main.dart
β”œβ”€β”€ core/
β”‚   β”œβ”€β”€ api_client.dart        # Dio setup with JWT interceptor
β”‚   └── constants.dart         # base URL, colors
β”œβ”€β”€ models/
β”‚   β”œβ”€β”€ tutorial.dart
β”‚   β”œβ”€β”€ material.dart
β”‚   └── assignment.dart
β”œβ”€β”€ screens/
β”‚   β”œβ”€β”€ login_screen.dart
β”‚   β”œβ”€β”€ home_screen.dart        # bottom nav container
β”‚   β”œβ”€β”€ tutorials/
β”‚   β”‚   β”œβ”€β”€ tutorials_list.dart
β”‚   β”‚   └── tutorial_player.dart
β”‚   β”œβ”€β”€ materials/
β”‚   β”‚   β”œβ”€β”€ materials_list.dart
β”‚   β”‚   └── material_viewer.dart # opens drive in-app browser
β”‚   β”œβ”€β”€ assignments/
β”‚   β”‚   β”œβ”€β”€ assignments_list.dart
β”‚   β”‚   └── assignment_form.dart # webview for Google Form
β”‚   └── ai_correct/
β”‚       └── ai_correct_screen.dart
└── widgets/
    β”œβ”€β”€ tutorial_card.dart
    β”œβ”€β”€ material_card.dart
    └── assignment_card.dart

🧭 Bottom Navigation Structure

// home_screen.dart
final screens = [
  TutorialsListScreen(),    // Tab 1: 🎬 Tutorials
  MaterialsListScreen(),    // Tab 2: πŸ“š Materials
  AssignmentsListScreen(),  // Tab 3: πŸ“ Assignments
  AiCorrectScreen(),        // Tab 4: πŸ€– AI Correct
];

BottomNavigationBar(
  items: [
    BottomNavigationBarItem(icon: Icon(Icons.play_circle), label: 'Tutorials'),
    BottomNavigationBarItem(icon: Icon(Icons.menu_book), label: 'Materials'),
    BottomNavigationBarItem(icon: Icon(Icons.assignment), label: 'Assignments'),
    BottomNavigationBarItem(icon: Icon(Icons.auto_fix_high), label: 'AI Correct'),
  ],
)
06

YouTube Integration

▢️ How Unlisted YouTube Videos Work in App

You upload videos to YouTube as Unlisted. The URL is not searchable β€” only people with the link can watch. You store that URL/video ID in your admin panel. The Flutter app plays it with youtube_player_flutter directly inside the app β€” no redirect to YouTube app needed.

πŸ’‘ Set your YouTube videos as Unlisted (not Private, not Public). Private videos cannot be embedded. Unlisted videos can be played via the YouTube iframe/player API without any Google API key.

🎬 Tutorial Player Screen

// tutorial_player.dart
import 'package:youtube_player_flutter/youtube_player_flutter.dart';

class TutorialPlayerScreen extends StatefulWidget {
  final Tutorial tutorial;

  String extractVideoId(String url) {
    // Extracts video ID from full YouTube URL
    return YoutubePlayer.convertUrlToId(url) ?? url;
  }

  @override
  _state createState() {
    final videoId = extractVideoId(widget.tutorial.youtubeUrl);
    final controller = YoutubePlayerController(
      initialVideoId: videoId,
      flags: const YoutubePlayerFlags(
        autoPlay: true,
        mute: false,
        enableCaption: true,
      ),
    );

    // In build():
    return YoutubePlayerBuilder(
      player: YoutubePlayer(controller: controller),
      builder: (context, player) => Scaffold(
        appBar: AppBar(title: Text(widget.tutorial.title)),
        body: Column(children: [
          player,
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(widget.tutorial.description),
          )
        ]),
      ),
    );
  }
}

πŸ”‘ Admin Panel β€” Add Tutorial

In your admin panel, you paste the full YouTube URL. The backend stores it. The app extracts the video ID automatically using YoutubePlayer.convertUrlToId().

Supported URL formats: https://youtu.be/VIDEO_ID or https://www.youtube.com/watch?v=VIDEO_ID

07

Google Drive Materials

πŸ“„ How Drive Links Work

You share Google Drive files with "Anyone with the link can view" permission. Paste the share link in admin panel. The app opens the file using one of two strategies:

  • In-App WebView β€” opens the Drive preview page inside the app using webview_flutter. Students stay in the app.
  • url_launcher β€” opens in the device browser. Simpler but leaves the app.

Recommendation: Use WebView for PDFs β€” students stay in your app experience.

🌐 Drive WebView Viewer

// material_viewer.dart
import 'package:webview_flutter/webview_flutter.dart';

class MaterialViewerScreen extends StatefulWidget {
  final LearningMaterial material;

  @override
  State createState() {
    // Convert share URL to direct viewer URL
    // From: https://drive.google.com/file/d/FILE_ID/view?usp=sharing
    // To:   https://drive.google.com/file/d/FILE_ID/preview
    String getPreviewUrl(String driveUrl) {
      final uri = Uri.parse(driveUrl);
      final pathParts = uri.pathSegments; // ['file','d','FILE_ID','view']
      if (pathParts.contains('d')) {
        final fileId = pathParts[pathParts.indexOf('d') + 1];
        return 'https://drive.google.com/file/d/$fileId/preview';
      }
      return driveUrl; // fallback
    }

    final controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse(getPreviewUrl(material.driveUrl)));

    return Scaffold(
      appBar: AppBar(title: Text(material.title)),
      body: WebViewWidget(controller: controller),
    );
  }
}
08

Google Forms Assignments

πŸ“ How Forms Work in the App

You create a Google Form. Students can submit their responses. You copy the form link from admin panel. The app opens it in a WebView β€” students complete the form inside the app.

⚠️ Google Forms automatically records the email of logged-in Google accounts. If you want to see which student submitted, ask students to use their personal Google account, or add a "Name" field in your form.

🌐 Assignment Form WebView

// assignment_form.dart
class AssignmentFormScreen extends StatefulWidget {
  final Assignment assignment;

  @override
  State createState() {
    // Replace /edit with /viewform for clean submission URL
    String getFormUrl(String url) {
      return url
        .replaceAll('/edit', '/viewform')
        .replaceAll('/closedform', '/viewform');
    }

    final controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse(getFormUrl(assignment.formUrl)));

    return Scaffold(
      appBar: AppBar(title: Text(assignment.title)),
      body: WebViewWidget(controller: controller),
    );
  }
}
09

Grok AI Sentence Correction

πŸ€– Backend Proxy Route (Node.js)

The Flutter app sends the student's sentence to YOUR backend. Your backend forwards it to Grok API (with your secret key). Response comes back to app. Key is never exposed to app.

// src/routes/ai.js
const express = require('express');
const axios = require('axios');
const authMiddleware = require('../middleware/auth');
const router = express.Router();
const db = require('../db');

router.post('/correct', authMiddleware, async (req, res) => {
  const { sentence } = req.body;

  if (!sentence || sentence.trim().length === 0) {
    return res.status(400).json({ error: 'Sentence is required' });
  }

  const prompt = `You are an English teacher. A student wrote:
"${sentence}"

Please:
1. Correct any grammar, spelling, or punctuation errors.
2. Provide the corrected sentence.
3. Explain each mistake briefly in simple English.
4. Give 1-2 better alternative ways to say the same thing.

Format your response as JSON:
{
  "corrected": "...",
  "mistakes": ["mistake 1", "mistake 2"],
  "alternatives": ["alt 1", "alt 2"]
}`;

  try {
    const response = await axios.post(
      'https://api.x.ai/v1/chat/completions',
      {
        model: 'grok-3',
        messages: [{ role: 'user', content: prompt }],
        temperature: 0.3,
        max_tokens: 500,
      },
      {
        headers: {
          'Authorization': `Bearer ${process.env.GROK_API_KEY}`,
          'Content-Type': 'application/json',
        },
      }
    );

    const content = response.data.choices[0].message.content;
    const cleanJson = content
      .replace(/```json/g, '')
      .replace(/```/g, '')
      .trim();
    const parsed = JSON.parse(cleanJson);

    // Save to history (optional)
    await db.execute(
      'INSERT INTO ai_corrections (student_id, original_text, corrected_text) VALUES (?,?,?)',
      [req.studentId, sentence, parsed.corrected]
    );

    res.json(parsed);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'AI correction failed' });
  }
});

module.exports = router;

πŸ“± Flutter AI Correct Screen

// ai_correct_screen.dart
class AiCorrectScreen extends StatefulWidget {
  final controller = TextEditingController();
  Map<String, dynamic>? result;
  bool isLoading = false;

  Future<void> correctSentence() async {
    setState(() => isLoading = true);
    final response = await ApiClient.instance.post('/ai/correct', {
      'sentence': controller.text
    });
    setState(() {
      result = response.data;
      isLoading = false;
    });
  }

  // UI in build():
  Column(children: [
    TextField(
      controller: controller,
      maxLines: 4,
      decoration: InputDecoration(
        hintText: 'Type your English sentence here...',
        border: OutlineInputBorder(),
      ),
    ),
    ElevatedButton(
      onPressed: correctSentence,
      child: Text('Check & Correct'),
    ),
    if (result != null) ...[
      Card(child: Column(children: [
        Text('βœ… Corrected: ${result!["corrected"]}'),
        Text('πŸ“Œ Mistakes:'),
        ...(result!['mistakes'] as List)
          .map((m) => Text('β€’ $m')),
        Text('πŸ’‘ Alternatives:'),
        ...(result!['alternatives'] as List)
          .map((a) => Text('β†’ $a')),
      ])),
    ]
  ])
}
10

Admin Panel (React Web App)

πŸ–₯️ Admin Panel Pages

  • Login β€” Single admin login (hardcoded credentials or env var, not in DB)
  • Tutorials β€” List with Add/Edit/Delete. Fields: Title, Description, YouTube URL, Category, Sort Order, Published toggle
  • Materials β€” Same pattern. Fields: Title, Description, Google Drive URL, File Type, Category
  • Assignments β€” Fields: Title, Description, Google Form URL, Due Date, Max Marks
  • Students β€” View registered students, activate/deactivate, reset password

πŸ” Admin Auth Strategy

// adminAuth.js middleware
module.exports = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  if (!decoded.isAdmin) {
    return res.status(403).json({ error: 'Admin access only' });
  }
  next();
};

// Admin login route β€” no DB needed, use env vars
router.post('/admin/login', (req, res) => {
  const { username, password } = req.body;
  if (
    username === process.env.ADMIN_USERNAME &&
    bcrypt.compareSync(password, process.env.ADMIN_PASSWORD_HASH)
  ) {
    const token = jwt.sign(
      { isAdmin: true },
      process.env.JWT_SECRET,
      { expiresIn: '12h' }
    );
    res.json({ token });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});
11

Student Auth & Management

πŸ”‘ Student Login Flow

// POST /auth/login
router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const [rows] = await db.execute(
    'SELECT * FROM students WHERE email = ? AND is_active = 1',
    [email]
  );
  if (!rows.length || !bcrypt.compareSync(password, rows[0].password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const student = rows[0];
  const token = jwt.sign(
    { studentId: student.id, name: student.name },
    process.env.JWT_SECRET,
    { expiresIn: '30d' }
  );
  res.json({ token, name: student.name });
});

// JWT auth middleware
// src/middleware/auth.js
module.exports = (req, res, next) => {
  try {
    const token = req.headers.authorization.split(' ')[1];
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.studentId = decoded.studentId;
    next();
  } catch {
    res.status(401).json({ error: 'Unauthorized' });
  }
};

πŸ‘₯ Student Onboarding Options

You have two options for how students get accounts:

  • Option A β€” You create accounts manually: In admin panel, you add student name, email, phone and set a password. Share credentials with students. Simplest for a small class.
  • Option B β€” Self registration with approval: Students register in app. You approve them in admin panel. More scalable but requires moderation.
12

Costs & Google Licences

πŸ’° Monthly Cost Breakdown

ServiceWhat You UseCostNotes
OCI Always Free VM Node.js backend + MySQL + Admin panel hosting β‚Ή0 You already have this. 2 OCPU, 12GB RAM.
YouTube Unlisted video hosting β‚Ή0 Free for uploading. No API key needed for player.
Google Drive PDF/material hosting β‚Ή0 (up to 15GB) Free personal Google Drive is enough for materials.
Google Forms Assignment submissions β‚Ή0 Completely free. Results in Google Sheets.
Grok xAI API Sentence correction module ~β‚Ή0.2–0.5/call Grok-3: $3 per 1M input tokens. Each call ~200–500 tokens. 100 calls/day β‰ˆ $1–3/month β‰ˆ β‚Ή100–250.
Domain (optional) yourappname.com for admin panel β‚Ή700–1200/year Optional. Can use server IP directly if budget is tight.
SSL Certificate HTTPS for your API β‚Ή0 Use Let's Encrypt via Certbot. Free and auto-renews.
Total (without Play Store) ~β‚Ή100–300/month Only Grok API is a recurring cost.

πŸͺ Google Play Store β€” One-Time Cost

ItemCostNotes
Google Play Developer Account $25 (β‰ˆ β‚Ή2,100) One-time lifetime fee. You can publish unlimited apps.
App signing (keystore) β‚Ή0 Use Flutter's keytool to generate. Keep backup of keystore!
APK review time β€” 3–7 days for first submission. Updates usually 1–2 days.
πŸ’‘ Alternative to Play Store: For a small class, you can skip Play Store entirely. Build a release APK using flutter build apk --release and share it directly via WhatsApp or Google Drive link. Students enable "Install from unknown sources" and install. No β‚Ή2,100 needed. Only use Play Store if you plan to grow to public users.

πŸ“‹ Google Licences & Permissions Needed

What You NeedRequired?How to GetCost
YouTube Data API NOT needed The youtube_player_flutter package uses the standard YouTube player β€” no API key needed for playing unlisted videos in Flutter. β‚Ή0
Google Drive API NOT needed You open Drive share links in a WebView. No API integration required. No key, no quota. β‚Ή0
Google Forms API NOT needed You open Form URLs in WebView. Students submit directly on the Form page. Google Sheets records responses. β‚Ή0
Google Play Developer IF Play Store Register at play.google.com/console. Need a Google account + one-time fee. $25 once
Android App Permissions In app manifest Add INTERNET permission in AndroidManifest.xml. No special Google permission needed. β‚Ή0

πŸ“± AndroidManifest.xml β€” Required Permissions

<!-- android/app/src/main/AndroidManifest.xml -->
<manifest>
  <uses-permission android:name="android.permission.INTERNET"/>
  <!-- For opening Drive/Forms in WebView -->
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

  <application
    android:usesCleartextTraffic="true"   <!-- only if API is HTTP not HTTPS -->
    android:networkSecurityConfig="@xml/network_security_config"
    ...
  >
</manifest>

πŸš€ Implementation Timeline

  • 1

    Database & Backend API (3–4 days)

    Create MySQL schema, set up Node.js project, implement all routes with JWT auth, test with Postman.

  • 2

    Admin Panel React App (2–3 days)

    CRUD pages for tutorials, materials, assignments. Simple login. Deploy alongside backend on OCI VM.

  • 3

    Flutter App β€” Auth + Navigation (1–2 days)

    Login screen, JWT storage, bottom nav, API client with interceptor.

  • 4

    Tutorials + Materials + Assignments screens (2–3 days)

    YouTube player integration, Drive WebView, Forms WebView.

  • 5

    Grok AI Sentence Correction (1 day)

    Backend proxy route + Flutter UI with result display.

  • 6

    Testing + APK Build + Distribution (1–2 days)

    Test on real device, build release APK, share via WhatsApp or upload to Play Store.

⏱️ Total: ~10–15 days of focused development for a solo developer familiar with the stack.